<head>
  <meta charset="UTF-8">

  <style type="text/css">
    .note {
        color: #a0a0a0;
        font-size: 12px;
        font-style: italic;
    }

    table.callStackTable {
        width:100%;
    }
    table.callStackTable tr {
        background-color: #f0f0f0;
    }

    .prototype-warning {
        background-color:#ffb0b0; color:#ff0000; padding:4px; border:1px solid red; display:inline-block;
        font-size:13px;
    }

    .prototype-warning-small {
        background-color:#ffb0b0; color:#ff0000; padding:4px; border:1px solid red; display:inline-block;
        font-size:12px;
    }

    table tr {
        padding: 0;
    }

    table td {
        padding: 0;
    }

    .detail-text-light {
        font-size: 11px;
        color: #d0d0d0;
        text-align: center;
        font-style: italic;
    }

    #slider-threshold-handle {
        width: 3em;
        height: 1.4em;
        //top: 50%;
        margin-top: -.8em;
        text-align: center;
        line-height: 1.6em;
        font-size: 14px;
    }

    #slider-threshold-handle2 {
        width: 3em;
        height: 1.4em;
        //top: 50%;
        margin-top: -.8em;
        text-align: center;
        line-height: 1.6em;
        font-size: 14px;
    }

    .stale-notice {
        color: red;
        font-weight: bold;
    }


    .toggler-container {
        border: 2px solid black;
        border-radius: 5px;
        display: inline-block;
        padding-top: 2px;
        padding-bottom: 1px;
    }

    a.toggler {
        background: #00df00;
        cursor: pointer;
        border: 2px solid white;
        border-right-width: 15px;
        padding: 0 5px;
        border-radius: 5px;
        text-decoration: none;
        transition: all .5s ease;
    }

    a.toggler.off {
        background: #d0d0d0;
        border-right-width: 2px;
        border-left-width: 15px;
    }

    #downsample-label {
        font-size: 12px;
        font-weight: bold;
        color: red;
        transition: all .5s ease;
    }

    #downsample-label.off {
        color: #a0a0a0;
    }

    #lock-z-scale-label {
        font-size: 12px;
        font-weight: bold;
        color: green;
        transition: all .5s ease;
    }

    #lock-z-scale-label.off {
        color: #a0a0a0;
    }

    .branch-table-weight-cell {
        font-size:10px;
        margin:0px;
        padding:0px;
    }

    /* note: this may not actually vertically center things */
    .vertical-middle-togglebox-div {
        border-radius: 3px;
        border: 1px solid #e0e0e0;
        display: inline-block;
        padding: 8px;
        vertical-align: middle;
    }

    .tooltip {
      position: relative;
      display: inline-block;
      /*border-bottom: 1px dotted black;*/
    }

    .tooltip .tooltiptext {
      visibility: hidden;
      width: 160px;
      background-color: black;
      color: #fff;
      text-align: center;
      border-radius: 6px;
      padding: 5px 0;

      position: absolute;
      z-index: 1;
      top: 100%;
      left: 50%; 
      margin-left: -80px;
    }

    .tooltip:hover .tooltiptext {
      visibility: visible;
    }

  </style>

  <script src="binary_buffer.js"></script>
  <script src="branch_trace_reader.js"></script>
  <script src="branch_profile.js"></script>

  <!-- Plotly.js -->
  <script src="plotly-latest.min.js"></script>
  
  <link rel="stylesheet" href="jquery-ui-1.12.1.custom/jquery-ui.css">
  <script src="jquery-3.4.1.min.js"></script>
  <script src="jquery-ui-1.12.1.custom/jquery-ui.js"></script>


  <link rel="stylesheet" href="ion-rangeslider/2.3.0/css/ion.rangeSlider.min.css"/>    
  <script src="ion-rangeslider/2.3.0/js/ion.rangeSlider.min.js"></script>


  <script src="http://culmat.github.io/jsTreeTable/treeTable.js"></script>

  <script>

    function toFixedIfFractional(n, digits=2) {
        if (Math.floor(n) != n)
            return n.toFixed(digits)
        else
            return n.toString()
    }

    // Get a css hex color for a given SHP weight. Color is based on correctness of weight (green for correct,
    // red for false) and interpolation between an unsaturated color for low-magnitude weights and a saturated
    // color for high-magnitude weights. colorSaturationWeight controls the absolute value weight at which the
    // color will be fully saturated.
    function colorForWeight(correct, weight, colorSaturationWeight) {
        if (correct) {
            const c = interpColor([240,255,240], [0,255,0], Math.min(1,(Math.abs(weight)/colorSaturationWeight)))
            return colorToHex(c)
        } else {
            const c = interpColor([255,240,240], [255,0,0], Math.min(1,(Math.abs(weight)/colorSaturationWeight)))
            return colorToHex(c)
        }    
    }

    function ensureSignChar(num) {
        let s = num.toString()
        if (s[0] != '-')
            s = '+' + s
        return s
    }

    function colorToHex(c) {
        let r = c[0].toString(16)
        if (r.length < 2)
            r = '0' + r
        let g = c[1].toString(16)
        if (g.length < 2)
            g = '0' + g
        let b = c[2].toString(16)
        if (b.length < 2)
            b = '0' + b

        return '#' + r + g + b
    }

    function interpColor(a, b, fraction) {
        const nc = [
            Math.round(Math.sqrt((a[0]**2 + (b[0]**2 - a[0]**2) * fraction))),
            Math.round(Math.sqrt((a[1]**2 + (b[1]**2 - a[1]**2) * fraction))),
            Math.round(Math.sqrt((a[2]**2 + (b[2]**2 - a[2]**2) * fraction)))
        ]
        return nc
    }

    let SNAPSHOT_TOKEN = 1
    let POINT_TOKEN = 0

    var DEBUG = true // disable to stop debug messages
    
    // TODO: these should come from loaded trace!
    var tablexdim = 1024
    var numBanks = 2
    var numTables = 16

    var xdim = tablexdim
    var ydim = numTables * numBanks

    var tStart = 0
    var tEnd = 2000000;

    var bStart = 0
    var bEnd = 2000000;
    var dbgEl = null;

    var ts = [] // weights
    var bs = [] // branches
    var dataChangedSinceRegenerate = true // has data changed since the Z(heatmap) data was regenerated

    var currentFile = null // Currently opened file (null is unintialized state orrandom data)

    var trackedCells = Array()
    var branchFilters = new Map() // branches being tracked specifically

    var needResetThresholdSlider = true

    var statId = 'accesses' // default. TODO: assign from select box at setup

    var downsampleHeatmap = false

    var heatmapLockZ = false // corresponds to toggle switch
    var heatmapZMin = 0
    var heatmapZMax = 0

    function logDebug(f) {
        if (!DEBUG)
            return;

        if (!dbgEl) {
            alert('unable to log debug because debug msg has not been set yet:' + f());
            return;
        }
        
        dbgEl.innerHTML = f() + "<br/>" + dbgEl.innerHTML.slice(0,1000);
    }

    // Indices along heatmap x and y axes
    var xValues = [...Array(xdim).keys()];
    var yValues = [...Array(ydim).keys()];

    var colorscaleValue = [
      [0, '#3D9970'],
      [1, '#001f3f']
    ];

    var colorScaleGradientBlueRed = [
        [0.0, 'rgb(0,0,255)'],
        [0.1, 'rgb(49,54,149)'],
        [0.2, 'rgb(69,117,180)'],
        [0.3, 'rgb(116,173,209)'],
        [0.4, 'rgb(171,217,233)'],
        [0.5, 'rgb(224,243,248)'],
        [0.6, 'rgb(254,224,144)'],
        [0.7, 'rgb(253,174,97)'],
        [0.8, 'rgb(244,109,67)'],
        [0.9, 'rgb(215,48,39)'],
        [1.0, 'rgb(255,0,0)'],
        ]

    var colorScaleGradientBlueGreenRed = [
        [0.0, 'rgb(180,180,255,0.3)'],
        [0.1, 'rgb(50,90,240,0.3)'],
        [0.2, 'rgb(50,110,220,0.3)'],
        [0.3, 'rgb(50,180,125,0.3)'],
        [0.4, 'rgb(50,190,100,0.3)'],
        [0.5, 'rgb(50,200,20,0.3)'],
        [0.6, 'rgb(100,200,0,0.3)'],
        [0.7, 'rgb(180,200,0,0.5)'],
        [0.8, 'rgb(255,200,0,0.7)'],
        [0.9, 'rgb(255,100,0,0.9)'],
        [1.0, 'rgb(255,0,0,1.0)'],
        ]

    // Logarithmic scale
    var colorScaleLogGradientBlueRed = [
        [0.0, 'rgb(0,0,255,0.6)'],
        [Math.log(1) / Math.log(10), 'rgb(49,54,149,0.6)'],
        [Math.log(2) / Math.log(10), 'rgb(69,117,180,0.6)'],
        [Math.log(3) / Math.log(10), 'rgb(116,173,209,0.6)'],
        [Math.log(4) / Math.log(10), 'rgb(171,217,233,0.6)'],
        [Math.log(5) / Math.log(10), 'rgb(224,243,248,0.6)'],
        [Math.log(6) / Math.log(10), 'rgb(254,224,144,0.6)'],
        [Math.log(7) / Math.log(10), 'rgb(253,174,97,0.6)'],
        [Math.log(8) / Math.log(10), 'rgb(244,109,67,0.6)'],
        [Math.log(9) / Math.log(10), 'rgb(215,48,39,0.6)'],
        [1.0, 'rgb(255,0,0,0.6)'],
        ]

    var colorScaleLogGradientBlueGreenRed = [
        [0.0,                        'rgb(0,0,255,0.6)'],
        [Math.log(1) / Math.log(10), 'rgb(49,50,149,0.6)'],
        [Math.log(2) / Math.log(10), 'rgb(37,100,112,0.6)'],
        [Math.log(3) / Math.log(10), 'rgb(24,150,75,0.6)'],
        [Math.log(4) / Math.log(10), 'rgb(11,205,34,0.6)'],
        [Math.log(5) / Math.log(10), 'rgb(0,200,0,0.6)'],
        [Math.log(6) / Math.log(10), 'rgb(85,200,12,0.6)'],
        [Math.log(7) / Math.log(10), 'rgb(170,200,24,0.6)'],
        [Math.log(8) / Math.log(10), 'rgb(255,200,37,0.6)'],
        [Math.log(9) / Math.log(10), 'rgb(255,100,49,0.6)'],
        [1.0,                        'rgb(255,0,0,0.6)'],
        ]

    // Which custom gradient to use typically
    var colorScaleGradient = colorScaleGradientBlueGreenRed


    function rgbStringToArray(cs) {
        return cs.replace('rgb','').replace('(','').replace(')','').split(',').map(function(v) {
            return Number(v);
        })
    }

    // Calculate the current heatmap color from a value n
    // TODO: should pre-sort colormap (to be safe) and transform colors into arrays for performance
    function currentHeatmapColor(n, fallbackColor='#ffffff') {
        if (isNaN(n))
            return fallbackColor

        if (n <= heatmapZMin)
            return colorToHex(rgbStringToArray(colorScaleGradient[0][1]))
        else if (n >= heatmapZMax)
            return colorToHex(rgbStringToArray(colorScaleGradient[colorScaleGradient.length - 1][1]))

        // Fraction of heatmap range
        const frac = (n - heatmapZMin) / (heatmapZMax - heatmapZMin)

        // Find first fraction entry in heatmap > `frac`. Assumes they are sorted
        for(let i = 1; i < colorScaleGradient.length; i++) {
            const f2 = colorScaleGradient[i][0]
            if (f2 > frac) {
                // Interpolate color from colormap entry i-1 and i.
                const f1 = colorScaleGradient[i-1][0]
                const ffrac = (frac - f1) / (f2 - f1)
                const c1 = rgbStringToArray(colorScaleGradient[i-1][1])
                const c2 = rgbStringToArray(colorScaleGradient[i  ][1])

                return colorToHex(interpColor(c1, c2, ffrac))
            }
        }

        console.error('invalid colormap: frac=' + frac)
    }


    // Layout parameters
    let tableAxisHeight = 350
    let heatmapRightMargin = 120
    let heatmapLeftMargin = 20

    var heatmapLayout = {
      title: '',
      //annotations: [],
      //width: 500,
      height: tableAxisHeight,
      margin: { t:0, l:heatmapLeftMargin, r:heatmapRightMargin, b:40},
      xaxis: {
        autorange: true, // gets near-pixel precision and less blurring compared to manual range
        //autorange: false, // performance
        //range: [0, xdim], // performance
        title: 'row ()',
        ticks: '',
        side: 'bottom'
      },
      yaxis: {
        autorange: true, // gets near-pixel precision and less blurring compared to manual range
        //autorange: false, // performance
        //range: [-0.5, ydim+0.5], // performance
        //title: 'table index',
        ticks: '',
        ticksuffix: ' ',
        //width: 700,
        //height: 700,
        autosize: true
      }
    };

    var tableDataLayout = {
      showlegend: false,
      title: 'Table Means',
      //annotations: [],
      //width: 500,
      height: tableAxisHeight,
      margin: { t:0, l:40, r:0, b:40},
      xaxis: {
        autorange: 'reversed',
        title: 'mean ???',
        ticks: '',
        side: 'bottom'
      },
      yaxis: {
        autorange: false, // performance
        range: [-0.5, ydim-0.25], // performance (and fixes alignment problems)
        title: 'table index (table# x nbanks + bank#)',
        ticks: '',
        ticksuffix: ' ',
        //width: 700,
        //height: 700,
        autosize: true
      }
    };

    var rowDataLayout = {
      showlegend: false,
      title: '',
      //annotations: [],
      //width: 500,
      height: 100,
      margin: { t:25, l:heatmapLeftMargin, r:heatmapRightMargin, b:0},
      xaxis: {
        visible: false,
        autorange: false,
        range: [-0.5, xdim+0.5],
        title: '',
        autotick: true,
        ticks: '',
        side: 'bottom'
      },
      yaxis: {
        autorange: true,
        title: '',
        ticks: '',
        ticksuffix: ' ',
        //width: 700,
        //height: 700,
        autosize: true
      }
    };

    function randX() {
        return Math.floor(Math.random() * xdim);
    }
    function randY() {
        return Math.floor(Math.random() * ydim);
    }

    // Generate some random timeseries data
    // ts = [ [t,x,y,w,c1,c2,c3], ...  ]
    // c1=unique accesses
    // c2=accesses
    // c3=delta weight
    // TODO: store snapshots in their OWN array for faster traversal. jumping randomly looking or steps is terrible. Js must have a good prefetcher since iteration of evrything is fast
    function generateRandomData() {
        ts = []
        bs = []
        let snapshotPeriod = 200000; // NOTE: snapshots do not appear to improve rendering performance except when 1m plus. why?

        // Note that using a flat array for the snapshot greatly improves iteration sped
        let tempZ = Array(ydim*xdim);
        tempZ.fill(0);

        let snapStart = tStart;
        let snapCount = 0
        for (var t = tStart; t < tEnd; t++) {
            x = randX();
            y = randY();
            w = Math.random()*100;
            
            // hide some high-ish frequency sinusoidal noise in upper tables. period is about 628k.
            if (y > 10) 
                c1 = Number(Math.sin(t / 100000) + 0.5 >= Math.random());
            else if (y > 5) 
                c1 = Number(Math.cos(t / 200000) + 0.5 >= Math.random());
            else
                c1 = Number(Math.random() > 0.5);
            if ((t % snapshotPeriod) == 0) {
                //logDebug(function(){return "snapshot=" + ts.length;});
                snapEnd = t;
                ts.push([SNAPSHOT_TOKEN, snapStart, snapEnd, tempZ, snapCount]);
                snapStart = t;
                tempZ = Array(ydim*xdim);
                tempZ.fill(0); // reset snapshot
                snapCount = 0

                // NOTE: skip an event this cycle just to keep each entries time matching its index for this prototype
            }
            else {
                bs.push([
                    ts.length, // cycle,
                    'pc', // pc,
                    t%2, // correct,
                    t%3, // taken,
                    t%4, // yout,
                    0, // theta_at_training,
                    0, // bias_at_training,
                    0, // shpq_weights_found,
                    1, // dynamic_state,
                    t%12, // indirect,
                    t%10, // uncond,
                    0, // bias_at_lookup,
                    'tgt', // tgt,
                    ts.length, // ts index
                    ])

                ts.push([POINT_TOKEN, t, x, y, bs.length - 1,
                     c1,
                     0, // c2
                     1, // pclo
                     2, // pchi
                     t%2, // correct
                     1 // new weight
                     ]);

                // TEMP: only tracking 1 counter for now. If more were needed, keep the array flat and index accordingly
                tempZ[y*xdim + x] += c1;
                snapCount++
            }
        }

        bEnd = bs.length - 1

        const slider = $(".js-range-slider").data('ionRangeSlider')
            const updates = {min: bStart, max: bEnd}
            if (slider.old_to > bEnd)
                updates['to'] = bEnd
            if (slider.old_from < bStart) {
                updates['from'] = bStart
            } else if (slider.old_from > bEnd) {
                updates['from'] = bStart // Old range was totally past new range
            }
            slider.update(updates)

        logDebug(function(){return "random data generated: ts=" + ts.length;});
    }


    function quickPlot(chunkedReader, version, afterLoad) {
        xs = []

        console.log('running quickPlot')

        // Data-reading and array-generation state
        const buf = new BinaryBuffer(chunkedReader);
        const reader = new BranchTraceReaderV5(buf);

        const mpBucket = new Bucket('mispred%', 100)
        const bucketSize = 10000
        let bucketOff = 0
        let cycleStart = 0
        let branchCount = 0

        function loadMore() {
            console.log('loadMore() xs=' + xs.length)
            while (1) {
                // Async load of data
                if (!reader.readyToRead()) {
                    if (reader.eof())
                        return // Nothing left to read
                    console.log('awaitReadyToRead() xs=' + xs.length)
                    reader.awaitReadyToRead(function(){setTimeout(loadMore,1)})
                    return
                }

                const entry = reader.nextRecord()
                if (entry == null)
                    break

                // TODO: check entry[0] for 'T' or 'W'
                if (entry[0] == 'T') { // Train event
                    cycle = entry[1]
                    pcLo = entry[2]
                    pcHi = entry[3]
                    correct = entry[4]
                    taken = entry[5]
                    yout = entry[6]
                    theta_at_training = entry[7]
                    bias_at_training = entry[8]

                    // new 
                    shpq_weights_found = entry[9]
                    dynamic_state = entry[10]
                    indirect = entry[11]
                    uncond = entry[12]

                    bias_at_lookup = entry[13]
                    tgtLo = entry[14]
                    tgtHi = entry[15]

                    mpBucket.add(!correct)

                    bucketOff++
                    //if (bucketOff == bucketSize) {
                    if (cycle - cycleStart > bucketSize) {
                        //xs.push(cycleStart)
                        xs.push(branchCount)
                        mpBucket.nextBucket()
                        bucketOff = 0
                        cycleStart = cycle
                    }

                    branchCount++

                } else if (entry[0] == 'W') { // Indicates a write to a weight row
                   // Nothing
                }
            }

            // Done
            var mpData = []
            let lineWidth = 0.6 // thickness of lines (<1 implies transparency)
            const template = {
                    name: 'Mean',
                    x: xs,
                    y: null,
                    type: 'line',
                    line: {
                        width: lineWidth,
                    }
                }

            const layout = {
            showlegend: true,
                title: '',
                height: 150,
                margin: { t:0, 
                          l:40, // roughly match td to the left of textbox
                          r:120, // roughly match window_size textbox
                          b:30},
                xaxis: {
                    autorange: false,
                    range: [0, bs.length],
                    title: 'mispredict rate',
                    ticks: '',
                    side: 'bottom'
                },
                yaxis: {
                    autorange: true,
                    title: 'table index',
                    ticks: '',
                    ticksuffix: ' ',
                    autosize: true
                }
            };

            mpData.push(mpBucket.plotlyTrace(template))
            Plotly.react('heatmapDiv', mpData, layout, {showSendToCloud: false});

            afterLoad()
        }

        reader.asyncInit(loadMore)
    }

    // Load data from a binary string and populate ts,tStart,tEnd, etc.
    // Update the slider to reflect new data x range
    function loadMassagedRealData(chunkedReader, version, afterLoad, skipBranches=0, maxNumBranches=-1) {

        dataChangedSinceRegenerate = true

        ts = [] // clear data
        bs = [] // clear branch array
        tStart = 0
        tEnd = 0

        // Snapshotting
        // Note that using a flat array for the snapshot greatly improves iteration speed
        let tempZ = Array(ydim*xdim);
        tempZ.fill(0);
        let tempZ2 = Array(ydim*xdim);
        tempZ2.fill(0);
        const snapshotPeriod = 200000;
        let snapStart = 0
        let snapCount = 0

        // State from last training event
        let cycle
        let pcLo
        let pcHi
        let correct
        let taken
        let yout
        let theta_at_training
        let bias_at_training

        // new
        let shpq_weights_found
        let dynamic_state
        let indirect
        let uncond


        let bi = 0 // branch index


        // Calculate the behavior of this row with respect to the prediction.
        // Weight=0 is considered 'incorrect' always
        // NOTE: captures taken and correct from training event variables (above)
        function getContribs(entry, taken, correct) {
            const lookup_weight = entry[3] 
            const contrib_mpred = !correct && ((!taken && lookup_weight > 0) || (taken && lookup_weight < 0));
            const contra_mpred = !correct && ((!taken && lookup_weight < 0) || (taken && lookup_weight > 0));
            return [contrib_mpred, contra_mpred]
        }

        function getWeightCorrect(entry, taken) {
            const lookup_weight = entry[3] 
            const weight_correct = ((lookup_weight > 0) && taken) || ((lookup_weight < 0) && !taken)
            return weight_correct
        }                
                

        // Functor for extracting data
        // Define a value and value2 based on the user's selected statistic
        let value
        let value2 = null
        let extractValues = null

        if (statId == 'accesses') {
            extractValues = function (entry, taken, correct) {
                value = 1
                value2 = null
            }
        } else if (statId == 'weight_change') {
            extractValues = function (entry, taken, correct) {
                const d_weight = entry[5]
                //value = weight - absWeight[y*xdim + x]
                value = d_weight
                value2 = null
            }
        } else if (statId == 'unique_pcs') {
            extractValues = function (entry, taken, correct) {
                value = entry[6]
                value2 = null
            }
        } else if (statId == 'thrash_1') {
            extractValues = function (entry, taken, correct) {
                value = entry[7]
                value2 = null
            }
        } else if (statId == 'weight_change_mag') {
            extractValues = function (entry, taken, correct) {
                const d_weight = entry[5]
                //value = Math.abs(weight - absWeight[y*xdim + x]);
                value = Math.abs(d_weight);
                value2 = null
            }
        } else if (statId == 'weight_mpreds') {
            extractValues = function (entry, taken, correct) {
                const weight_correct = getWeightCorrect(entry, taken)
                const weight_incorrect = Number(!weight_correct)
                value = weight_incorrect
                value2 = null
            }
        } else if (statId == 'weight_cpreds') {
            extractValues = function (entry, taken, correct) {
                const weight_correct = getWeightCorrect(entry, taken)
                value = weight_correct
                value2 = null
            }
        } else if (statId == 'contra_mispred') {
            extractValues = function (entry, taken, correct) {
                const contra_mpred = getContribs(entry, taken, correct)[1]
                value = contra_mpred
                value2 = null
            }
        } else if (statId == 'contrib_mispred') {
            extractValues = function (entry, taken, correct) {
                const contrib_mpred = getContribs(entry, taken, correct)[0]
                value = contrib_mpred
                value2 = null
            }
        } else if (statId == 'weight_accuracy') {
            extractValues = function (entry, taken, correct) {
                const weight_correct = getWeightCorrect(entry, taken)
                const weight_incorrect = Number(!weight_correct)
                value = weight_correct
                value2 = 1
            }
        } else if (statId == 'weight_change_events') {
            extractValues = function (entry, taken, correct) {
                const d_weight = entry[5]
                //value = (weight != absWeight[y*xdim + x]) // any change in weight
                value = (d_weight != 0) // any change in weight
                value2 = null
            }
        } else if (statId == 'mp_harm_rate') {
            extractValues = function (entry, taken, correct) {
                const p = getContribs(entry, taken, correct)
                const contrib_mpred = p[0]
                const contra_mpred = p[1]
                value = contrib_mpred
                value2 = contrib_mpred + contra_mpred
            }
        } else if (statId == 'mp_help_rate') {
            extractValues = function (entry, taken, correct) {
                const p = getContribs(entry, taken, correct)
                const contrib_mpred = p[0]
                const contra_mpred = p[1]
                value = contra_mpred
                value2 = contrib_mpred + contra_mpred
            }
        } else if (statId == 'mispred') { // TODO: should come from branch trace
            extractValues = function (entry, taken, correct) {
                value = !correct // from last train record
            }
        } else if (statId == 'weight_change_thrash') {
            extractValues = function (entry, taken, correct) {
                const d_weight = entry[5]
                //value = Math.abs(weight - absWeight[y*xdim + x]);
                //value2 = weight - absWeight[y*xdim + x]
                value = Math.abs(d_weight);
                value2 = d_weight
            }
        } else {
            console.error('unsupported stat id ' + statId)
        }


        // Data-reading and array-generation state
        const buf = new BinaryBuffer(chunkedReader);
        const reader = new BranchTraceReaderV5(buf);

        let t = 0
        let i = 0
        let totalBranches = 0 // including skipped

        function loadMore() {
            console.log('loadMore() i=' + i + ' t=' + t + ' brns=' + totalBranches + '/' + (skipBranches + maxNumBranches))
            while (1) {
                // Async load of data
                if (!reader.readyToRead()) {
                    if (reader.eof()) {
                        console.log('EOF: ' + reader.buffer.acr.filepos + '/' + reader.buffer.acr.filesize)
                        return // Nothing left to read
                    }
                    console.log('awaitReadyToRead() i=' + i + ' t=' + t + ' brns=' + totalBranches + ' of skip ' + skipBranches)
                    reader.awaitReadyToRead(function(){setTimeout(loadMore,1)})
                    return
                }

                const entry = reader.nextRecord()
                if (entry == null)
                    break

                if (totalBranches < skipBranches) {
                    if (entry[0] = 'T')
                        totalBranches++ // only count branches
                    continue;
                } else if (totalBranches == skipBranches) {
                    updateBusyBox(skipBranches + ' start records skipped')
                    console.log('done with skipping i=' + i + ' t=' + t + ' brns=' + totalBranches)
                    totalBranches++
                } else if (maxNumBranches != -1 && bs.length >= maxNumBranches) {
                    console.warn('reached final branch ' + i + ' in range ' + skipBranches + ' to ' + maxNumBranches + ' bs=' + bs.length + ' ts=' + ts.length)
                    break
                }

                // TODO: check entry[0] for 'T' or 'W'
                if (entry[0] == 'T') { // Train event
                    totalBranches++ // only count branches

                    cycle = entry[1]
                    pcLo = entry[2]
                    pcHi = entry[3]
                    correct = entry[4]
                    taken = entry[5]
                    yout = entry[6]
                    theta_at_training = entry[7]
                    bias_at_training = entry[8]

                    // new 
                    shpq_weights_found = entry[9]
                    dynamic_state = entry[10]
                    indirect = entry[11]
                    uncond = entry[12]

                    bias_at_lookup = entry[13]
                    tgtLo = entry[14]
                    tgtHi = entry[15]

                    // Branch trace
                    const pc = renderBranchAddress(pcLo, pcHi)
                    const tgt = renderBranchAddress(tgtLo, tgtHi)
                    bs.push([cycle, pc, correct, taken, yout, theta_at_training, bias_at_training, shpq_weights_found, dynamic_state, indirect, uncond, bias_at_lookup, tgt, ts.length])

                    bi++

                    continue

                } else if (entry[0] == 'W') { // Indicates a write to a weight row
                    let table = entry[1]
                    let row = entry[2]
                    let lookup_weight = entry[3]
                    let new_weight = entry[4]
                    let d_weight = entry[5] // this is the change in weight by training. NOT new_weight-lookup_weight
                    let d_unique = entry[6]
                    let thrash_1 = entry[7]                
                    
                    // Heatmap Display-space transformation of coordinates
                    y = table
                    x = row

                    extractValues(entry, taken, correct) // populate value and value2 using a closure to refer to 'T' event state
         
                    //// Track current new weight
                    //absWeight[y*xdim + x] = weight

                    if ((t % snapshotPeriod) == 0) {
                        //logDebug(function(){return "snapshot
                        snapEnd = t;
                        ts.push([SNAPSHOT_TOKEN, snapStart, snapEnd, tempZ, snapCount, tempZ2]);
                        console.log('snapshot ' + snapStart + ' end=' + snapEnd + ' count=' + snapCount)
                        snapStart = t;
                        tempZ = Array(ydim*xdim);
                        tempZ.fill(0);
                        tempZ2 = Array(ydim*xdim);
                        tempZ2.fill(0);
                        snapCount = 0


                        // WARNING: skip an event this cycle just to keep each entries time matching its index for this prototype
                        // TODO: this needs to be fixed and the "snapshot" needs to be stored in a separate structure
                    }
                    else {
                        ts.push([POINT_TOKEN, t, x, y,
                                 bi - 1, // corresponding branch
                                 value, value2,
                                 pcLo, pcHi, getWeightCorrect(entry), lookup_weight, d_weight])


                        // TEMP: only tracking 1 counter for now. If more were needed, keep the array flat and index accordingly
                        tempZ[y*xdim + x] += value
                        if (value2 != null)
                            tempZ2[y*xdim + x] += value2
                        snapCount++
                    }
                }

                t++
                i++
                if (i == 200000) {
                    console.log('updating progress i=' + i + ' t=' + t + ' brns=' + totalBranches)
                    updateBusyBox(t.toString() + " records")
                    i = 0
                    setTimeout(loadMore, 100)
                    return
                }
            }

            // Finished loading
            tEnd = ts.length

            bStart = 0
            bEnd = bs.length - 1

            // Reset slider and stuff. Adjust range to fit. This needs some thought to adjust prdictable
            const slider = $(".js-range-slider").data('ionRangeSlider')
            const updates = {min: bStart, max: bEnd}
            if (slider.old_to > bEnd)
                updates['to'] = bEnd
            if (slider.old_from < bStart) {
                updates['from'] = bStart
            } else if (slider.old_from > bEnd) {
                updates['from'] = bStart // Old range was totally past new range
            }
            slider.update(updates)
        
            afterLoad()
        }
       
        reader.asyncInit(loadMore)
    }


    function zeroZ() {
        z = [];
        for(const y of yValues) {
            z.push([]);
            zy = z[z.length - 1]
            for (const x of xValues) {
                zy.push(0);
            }
        }
        return z;
    }

    // Calculate stat value based on the given stat id, and a c1 and c2 involved in the calculation
    function calcStatValue(statId_, c1, c2) {
        let value
        if (statId == 'weight_change_thrash') {
            value = c1 / c2
        } else if (statId == 'weight_accuracy' || statId == 'mp_harm_rate' || statId == 'mp_help_rate') {
            value = Math.abs(c1 / c2)
        } else {
            value = c1
        }
        return value
    }

    function renderRowAccessProfile(filtTable, filtRow, filtBank) {
        // Clear existing stuff in case of failure
        $('#hm-row-profile-thead').html('')
        $('#hm-row-profile-tbody').html('')

        $('#hm-row-profile-stale').html('') // no longer stale
        $('#hm-row-profile-coords').html('[T:' + filtTable + ' R:' + filtRow + ' B' + filtBank + ']') 

        const thead = $('#hm-row-profile-thead')
        let theadStr = "<tr style='background-color:#c0c0f0;'>" +
                       "    <th>pc</th>" +
                       "    <th>bclass</th>" +
                       "    <th>accesses</th>" +
                       "    <th>net weight</th>" +
                       "    <th>sum abs weight chg</th>" +
                       "    <th>stat</th>"
        thead.html(theadStr)



        const prof = new Map()

        let slider = $(".js-range-slider").data('ionRangeSlider')
        let t0 = bs[slider.old_from][13] // ti
        let tE = bs[slider.old_to][13] // ti
        tE = adjustEndRangeToIncludeFullTraining(tE)

        let tblStr = ''
        for (var ti = t0; ti < tE; ti++) { // iterate all write events until numBranches reaches its limit
            var tv = ts[ti];
            if(ti < 0) throw 'negative ti';
            if (ti >= ts.length) throw 'oob ti;'
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1)
            if (isSnapshot) {
                // Skip snapshots. we want to real points only
            } else {
                // event
                let timestamp = tv[1]
                let x = tv[2]
                let y = tv[3] // table * nbanks + bank // encoded together
                let table = Math.floor(y/2) // TODO: move this math into a global function
                let bi = tv[4]
                let c1 = tv[5] // selected counter 1
                let c2 = tv[6] // selected counter 2
                //let pcLo = tv[7]
                //let pcHi = tv[8]
                let tableCorrect = tv[9]
                let tableLookupWeight = tv[10]
                let d_weight = tv[11]

                // filter table/row
                if (x != filtRow || y != (filtTable*numBanks + filtBank)) {
                    continue // skip this one
                }


                const pc = bs[bi][1]

                if (!prof.has(pc)) {
                    // Store [latest bi, sum c1, sum c2, instances, weight, weightmag]
                    prof.set(pc, [bi, c1, c2, 1, d_weight, Math.abs(d_weight)])
                } else {
                    const p = prof.get(pc)
                    p[0] = bi
                    p[1] += c1
                    p[2] += c2
                    p[3] += 1
                    p[4] += d_weight
                    p[5] += Math.abs(d_weight)
                }


            }
        }

        // render profile table
        const tbody = $('#hm-row-profile-tbody')
        let tbodyStr = ''
        
        prof.forEach(function(v,k,m) {

            const pc = k
            const bi = v[0]
            const c1 = v[1]
            const c2 = v[2]
            const instances = v[3]
            const d_weight = v[4]
            const abs_d_weight = v[5]

            // "Static" information from branch instance
            const b = bs[bi]
            const dynamic_state = b[8]
            const indirect = b[9]
            const uncond = b[10]
            bc = new BranchClass(dynamic_state, indirect, uncond)

            // Non-static information - specific to branch instance (do not use)
            //cycle = b2[0]
            //correct = b2[2]
            //taken = b2[3]
            //yout = b2[4]
            //bias_at_lookup = b2[11]
            //target = b2[12]

            const statVal = calcStatValue(statId, c1, c2)
            const color = currentHeatmapColor(statVal)
            tbodyStr +=
                '<tr>' + 
                '   <td>' + pc + '</td>' + 
                '   <td>' + bc.getString() + '</td>' +
                '   <td>' + instances + '</td>' +
                '   <td>' + ensureSignChar(d_weight) + '</td>' +
                '   <td>' + abs_d_weight + '</td>' +
                '   <td style="background-color:' + color + ';">' + statVal + '</td>' +
                '</tr>'
        })
        tbody.html(tbodyStr)

    }

    // Get an exclusive upper endpoint index into ts[] that includes all weight writes associated
    // with the branch found at ts[i]. If this cannot be done for some reason, returns ts.length
    function adjustEndRangeToIncludeFullTraining (i) {
        if (i >= ts.length)
            return ts.length

        let startBi = ts[i][4] // bi
        for (let ti = i+1; ti < ts.length; ti++) {
            if (ts[ti][4] != startBi) {
                return ti // start just before this change in branchIndex
            }
        }
        return ts.length
    }

    function adjustStartRangeToIncludeFullTraining (i) {
        if (i == 0)
            return 0

        let startBi = ts[i][4] // bi
        for (let ti = i; ti > 0; ti--) {
            if (ts[ti][4] != startBi) {
                return ti + 1 // start just after this change in branchIndex
            }
        }
        return 0
    }

    function clearBranchHistoryList() {
        $('#branch-history-stale').html('(STALE)')
        $('#branch-history-thead').html('')
        $('#branch-history-tbody').html('')
    }

    var lastBranchHistoryListFromStart = true
    function rerenderBranchHistoryListIfShown() {
        if ($('#branch-history-tbody').html() != '') // re-render only if already visible
            renderBranchHistoryList(lastBranchHistoryListFromStart)
    }

    function renderBranchHistoryList(fromStart) {
        lastBranchHistoryListFromStart = fromStart

        // Clear existing stuff in case of failure
        $('#branch-history-thead').html('')
        $('#branch-history-tbody').html('')

        $('#branch-history-stale').html('') // no longer stale

        let thead = $('#branch-history-thead')[0]
        theadStr = "<tr style='background-color:#c0c0f0;'>" +
                    "    <th>trn cyc</th>" +
                    "    <th>pc</th>" +
                    "    <th>bclass</th>" +
                    "    <th>target</th>" +
                    "    <th>Tkn</th>" +
                    "    <th style='font-size:11px;'>pred</th>" +
                    "    <th>yout</th>" +
                    "    <th>bias</th>" +
                    "    <th>wsum</th>"
        theadStr += "    <th>0</th><th>1</th>"
        theadStr += "    <th style='text-align:center;' colspan=" + (numTables - 4) + ">... per-table weight @lookup ...</th>"
        theadStr += "    <th>" + (numTables - 2) + "</th><th>" + (numTables - 1) + "</th>"
        theadStr += "    <th style='text-align:center;' colspan=" + numTables + ">row &quot;<i>" + statId + "</i>&quot; in selected range</th>"
        theadStr += "</tr>"
        thead.innerHTML = theadStr

        const showNonWriteBranches = $('#branch-history-show-no-write-branches-checkbox').is(":checked")
        const showOnlyWrongBranches = $('#branch-history-show-only-wrong-branches-checkbox').is(":checked")

        const MAX_BRANCHES = 200
        let numBranches = 0

        // Accumulated stats for last branch
        let numWrites = 0
        let weight_sum = 0
        let tblWeights = Array(numTables)
        tblWeights.fill(null)
        let stats = Array(numTables)
        stats.fill(null)
        let rows = Array(numTables)
        rows.fill(null)
        let banks = Array(numTables)
        banks.fill(null)

        // Latest branch info
        let latestBranchIndex = -1
        let cycle
        let pc
        let correct
        let taken
        let yout
        let bias_at_lookup
        let target
        let bc = null

        // Branch history
        let brns = []

        let slider = $(".js-range-slider").data('ionRangeSlider')
        let tS = bs[slider.old_from][13] // ti
        let tE = bs[slider.old_to][13] // ti
        tS = adjustStartRangeToIncludeFullTraining(tS)
        tE = adjustEndRangeToIncludeFullTraining(tE)

        let iterator
        let ti
        if (fromStart) {
            ti = bs[Math.min(slider.old_from + MAX_BRANCHES, bs.length-1)][13]
        } else {
            ti = tE - 1
        }

        let tblStr = ''
        for (; ti >= tStart; ti--) { // iterate all write events until numBranches reaches its limit

            var tv = ts[ti];
            if(ti < 0) throw 'negative ti';
            if (ti >= ts.length) throw 'oob ti;'
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1)
            if (isSnapshot) {
                // Skip snapshots. we want to real points only
            } else {
                // event
                let timestamp = tv[1]
                let x = tv[2]
                let y = tv[3] // table * nbanks + bank // encoded together
                let table = Math.floor(y/2) // TODO: move this math into a global function
                let bi = tv[4]
                let c1 = tv[5] // selected counter 1
                let c2 = tv[6] // selected counter 2
                //let pcLo = tv[7]
                //let pcHi = tv[8]
                let tableCorrect = tv[9]
                let tableLookupWeight = tv[10]

                while (bi != latestBranchIndex) {
                    // Change in branches
                    
                    if (latestBranchIndex != -1) {
                        //brns.push([pc, bc, correct, taken, yout, tblWeights, stats, rows])
                        //
                        const filterHasPC = branchFilters.has(pc)
                        if (branchFilters.size > 0 && !filterHasPC) {
                            // Filtered... skip
                        } else if (numWrites == 0 && !showNonWriteBranches) {
                            // Skip this line
                        } else if(correct && showOnlyWrongBranches) {
                            // skip this line
                        } else {
                            // Render the branch as a table row
                            let bgColor
                            let chColor
                            let chWeight
                            if (correct) {
                                bgColor = '#c0ffc0'
                                chColor = '#000000'
                                chWeight = 'normal'
                            } else {
                                bgColor = '#ff0000'
                                chColor = '#ffffff'
                                chWeight = 'bold'
                            }

                            tblStr += '<tr>' + 
                            // TODO: add an index here (to make loops easier to spot)
                              '<td>' + cycle + '</td>' +
                              '<td>' + pc + '</td>' +
                              '<td>' + bc.getString() + '</td>'
                            tblStr += '<td>' + target + '</td>'

                            // actual taken
                            if (taken) {
                                tblStr += '<td>T</td>'
                            } else {
                                tblStr += '<td>N</td>'
                            }

                            // prediction
                            tblStr += '<td style="font-weight:' + chWeight + '; color:' + chColor + '; background-color:' + bgColor + ';">'
                            if (correct == taken) {
                                tblStr += 'T'
                            } else {
                                tblStr += 'N'
                            }
                            tblStr += '</td>'

                            let youtColor = colorForWeight(correct, yout, 150)
                            tblStr += '<td style="background-color:' + youtColor + ';font-weight:bold;">' + ensureSignChar(yout) + '</td>'

                            let colorForBias = colorForWeight((bias_at_lookup >= 0 && taken) || (bias_at_lookup < 0 && !taken), bias_at_lookup, 50)
                            tblStr += '<td style="background-color:' + colorForBias + ';">' + ensureSignChar(bias_at_lookup) + '</td>'


                            if (numWrites > 0) {
                                // Sum of weights
                                const wsum = tblWeights.reduce((a,b) => a + b, 0)
                                let colorForWeightSum = colorForWeight((wsum >= 0 && taken) || (wsum < 0 && !taken), wsum, 50)
                                tblStr += '<td style="background-color:' + colorForWeightSum + ';">' + ensureSignChar(wsum) + '</td>'

                                // Show each table
                                for (const i of [...Array(numTables).keys()]) {
                                    const w = tblWeights[i]
                                    let wStr
                                    if (w == null)
                                        wStr = 'x'
                                    else
                                        wStr = ensureSignChar(w)
                                    let color = colorForWeight((w >= 0 && taken) || (w < 0 && !taken), w, 50)
                                    tblStr += '<td style="background-color:' + color + ';" class="branch-table-weight-cell">' + wStr  + '</td>'
                                }
                                for (const i of [...Array(numTables).keys()]) {
                                    // Show stat for row across selected time range (from the heatmap)
                                    const r = rows[i]
                                    const b = banks[i]
                                    const yIndex = i * numBanks + b
                                    let z = lastZOut[yIndex][r]
                                    const zColor = currentHeatmapColor(z)
                                    let zs = ''
                                    if (isNaN(z))
                                        zs = ''
                                    else
                                        zs = toFixedIfFractional(z)
                                    //tblStr += '<td class="branch-table-weight-cell" style="background-color:' + zColor + ';">' + zs + '</td>'

                                    const rowStr = "table " + i + ", bank " + b + ", row " + r
                                    tblStr += 
                                        '<td class="branch-table-weight-cell" style="background-color:' + zColor + ';">' + 
                                        '  <div class="tooltip">' + 
                                        '    <a href="javascript:void(0);" style="text-decoration:none;" onclick="renderRowAccessProfile(' + i + ',' + r + ',' + b + ');">' + zs + '</a>' +
                                        '    <span class="tooltiptext">' + rowStr + '<br/>click to profile tbl row</span>' +
                                        '  </div>' +
                                        '</td>'

                                    // Show stat for just this branch just this row (boring)
                                    //const n = Number(stats[i])
                                    //let ns = ''
                                    //if (isNaN(n))
                                    //    ns = ''
                                    //else
                                    //    ns = n.toString()
                                    //tblStr += '<td class="branch-table-weight-cell">' + ns + '</td>'
                                }
                            } else {
                                const numOtherCols = numTables * 2 + 1
                                tblStr += '<td colspan=' + numOtherCols + ' class="detail-text-light">branches without weight updates ignored</td>'
                            }
                            tblStr += '</tr>'


                            // Clear counters for this branch
                            numWrites = 0
                            weight_sum = 0
                            tblWeights = Array(numTables)
                            tblWeights.fill(null)
                            stats = Array(numTables)
                            stats.fill(null)
                            rows = Array(numTables)
                            rows.fill(null)
                            banks = Array(numTables)
                            banks.fill(null)

                            // Move on to the next branch
                            numBranches++
                            if (numBranches > MAX_BRANCHES)
                                break
                        }
                    }

                    // Store the new branch state
                    if (latestBranchIndex != -1)
                        latestBranchIndex--
                    else 
                        latestBranchIndex = bi

                    const b2 = bs[latestBranchIndex]
                    cycle = b2[0]
                    pc = b2[1]
                    correct = b2[2]
                    taken = b2[3]
                    yout = b2[4]
                    bias_at_lookup = b2[11]
                    target = b2[12]
                    const dynamic_state = b2[8]
                    const indirect = b2[9]
                    const uncond = b2[10]
                    bc = new BranchClass(dynamic_state, indirect, uncond)
                }

                if (numBranches > MAX_BRANCHES) // outer-loop (ts) break
                    break

                // Store this table's info so it can be included with the branch
                numWrites++
                weight_sum += tableLookupWeight
                tblWeights[table] = tableLookupWeight
                stats[table] = calcStatValue(statId, c1, c2)
                rows[table] = x
                banks[table] = y%numBanks
            }
        }

        let tbody = $('#branch-history-tbody')[0]
        tbody.innerHTML = tblStr
    }

    // Render a profile of the branches in the selection
    //
    // NEED BRANCH IMEM to include 
    //    - accuracy from all branches (not just training event)
    //    - sort by mpred%
    //    - sort by mpred COUNT
    function renderBranchProfileList() {
        $('#branch-imem-stale').html('') // no longer stale

        clearBranchFilters() // reset

        let bpm = new BranchProfileManager()

        let slider = $(".js-range-slider").data('ionRangeSlider')
        let t0 = bs[slider.old_from][13] // ti
        let tE = bs[slider.old_to][13] // ti

        for (var ti = tE-1; ti >= t0; ti--) {
            var tv = ts[ti];
            if(ti < 0) throw 'negative ti';
            if (ti >= ts.length) throw 'oob ti;'
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1)
            if (isSnapshot) {
                // Skip snapshots. we want to real points only
            } else {
                // event
                let timestamp = tv[1]
                let x = tv[2]
                let y = tv[3]
                let bi = tv[4]
                let c1 = tv[5] // selected counter 1
                let c2 = tv[6] // selected counter 2

                let pcLo = tv[7]
                let pcHi = tv[8]
                let correct = tv[9]

                const b = bs[bi]


                const dynamic_state = b[8]
                const indirect = b[9]
                const uncond = b[10]
                const bc = new BranchClass(dynamic_state, indirect, uncond)

                const branchCorrect = b[2]
                bpm.addBranches(renderBranchAddress(pcLo, pcHi), 1, c1, c2, correct, bc, branchCorrect)
            }
        }

        let thead = $('#branch-imem-thead')[0]
        thead.innerHTML = "<tr style='background-color:#c0c0f0;'>" +
                    "    <th>+filter</th>" +
                    "    <th>pc</th>" +
                    "    <th>bclass</th>" +
                    "    <th>count</th>" +
                    "    <th>" + statId + "</th>" +
                    "    <th>wght correct%</th>" +
                    "    <th>correct%</th>" +
                    "</tr>"

        let tblStr = ''
        let bplist = bpm.getDescendingListByMispredictCount() // by mispred count
        for (let bp of bplist) {
            const value = calcStatValue(statId, bp.count1, bp.count2).toFixed(2)
            const bclassStr = bp.bclass.getString()

            let weightCorrectPredRate = (100. * bp.weightCorrect / bp.instances).toFixed(2) + '%'
            let branchCorrectPredRate = (100. * bp.branchCorrect / bp.instances).toFixed(2) + '%'
            let idAdd = bp.pcStr + '-add-filter'
            let idRem = bp.pcStr + '-remove-filter'
            tblStr += '<tr>' + 
                      '<td>' + 
                      '<a id="' + idAdd + '" href="javascript:void(0);" onclick="addBranchFilter(\''+bp.pcStr+'\',\'' + idAdd + '\',\'' + idRem + '\');">[+]</a>' + 
                      '<a id="' + idRem + '" href="javascript:void(0);" onclick="remBranchFilter(\''+bp.pcStr+'\',\'' + idAdd + '\',\'' + idRem + '\');" style="display:none">[-]</a>' + 
                      '</td>' +
                      '<td>' + bp.pcStr + '</td><td>' + bclassStr + '</td><td>' + bp.instances + '</td>' +
                      '<td>' + value + '</td>' + 
                      '<td>' + weightCorrectPredRate + '</td>' + 
                      '<td>' + branchCorrectPredRate + '</td>' + 
                      '</tr>'
        }

        let tbody = $('#branch-imem-tbody')[0]
        tbody.innerHTML = tblStr
    }

    function generateRowAccessTimeline() {
        Plotly.purge('rowAccessTimelineDiv');
        setTimeout(renderRowAccessTimeline, 1)
        $('#branch-write-timeline-stale').html('') // no longer stale
    }

    function clearRowAccessTimeline() {
        $('#branch-write-timeline-stale').html('(STALE)') // no longer stale
        Plotly.purge('rowAccessTimelineDiv');
    }

    // Generate a row access timeline
    function renderRowAccessTimeline() {
        let slider = $(".js-range-slider").data('ionRangeSlider')
        let t0 = bs[slider.old_from][13] // ti
        let tE = bs[slider.old_to][13] // ti

        if (tE - t0 > 1000000) {
            alert('truncating window size to 1M pts')
            t0 = tE - 1000000
        }

        // WARNING: assumes 0 or 1 event per cycle.
        let rows = Array()
        let xs = Array()
        let vals = Array()
        for (var ti = tE-1; ti >= t0; ti--) {
            var tv = ts[ti];
            if(ti < 0) throw 'negative ti';
            if (ti >= ts.length) throw 'oob ti;'
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1)
            if (isSnapshot) {
                // Skip snapshots. we want to real points only
            } else {
                // event
                let timestamp = tv[1]
                let x = tv[2];
                let y = tv[3];
                let c1 = tv[5];
                if (tv.length > 6) {
                    let c2 = tv[6]
                }

                // TODO: combine stat 1 and 2.... for now just using first value for coloring?

                let flatRowIdx = y*xdim + x // (table*nbanks)+bank * nrows = flatRowIdx
                xs.push(ti) //xs.push(timestamp)
                rows.push(flatRowIdx)
                vals.push(c1)
            }
        }

        let rowTimelineLayout = {
          showlegend: false,
          title: 'Row timeline',
          //annotations: [],
          height: 800,
          margin: { t:0, l:40, r:0, b:40},
          xaxis: {
            autorange: true,
            title: 'branch#',
            ticks: '',
            side: 'bottom'
          },
          yaxis: {
            autorange: true,
            title: 'row (flat index)',
            ticks: '',
            ticksuffix: ' ',
            autosize: true
          }
        };

        // TODO: color rows per table?
        let rowTimelineData = [{
          x: xs,
          y: rows,
          type: 'scattergl',
          mode: 'markers',
          marker: {
            size: 1,
            color: vals,
            colorscale: colorScaleGradient,
          },
        }];

        Plotly.react('rowAccessTimelineDiv', rowTimelineData, rowTimelineLayout, {showSendToCloud: false});
    }
    

    // Regenerate Z from scratch
    // TODO: implement regenerate based on window deltas (.e.g add/subtract)
    // TODO: or... at least do not recreate z, just overwrite values with 0 and proceed
    var prevWinStart = 0
    var prevWinEnd = 0
    var prevZ = null
    var prevZ2 = null
    var lastZOut = null // latest data rendered
    var prevPerRowStats = null
    var prevPerRowStats2 = null
    var prevPerTableStats = null
    var prevPerTableStats2 = null
    var prevPoints = 0
    function regenerateZ(biEnd, windowSize = 1000, forceRecalc=false) {
        markProfilesAsStale()

        var t0 = Math.max(tStart, bs[biEnd - windowSize][13]);
        var tE = Math.min(tEnd, bs[biEnd][13]);

        let z
        let z2
        let perTableStats
        let perRowStats
        let points = prevPoints
        if (prevZ == null || tE < prevWinStart || t0 > prevWinEnd || forceRecalc || branchFilters.size != 0 || dataChangedSinceRegenerate) {
            // regenerate z data
            z = zeroZ();
            z2 = zeroZ();
            perTableStats = Array(ydim);
            perTableStats.fill(0)
            perTableStats2 = Array(ydim);
            perTableStats2.fill(0)
            perRowStats = Array(xdim);
            perRowStats.fill(0)
            perRowStats2 = Array(xdim);
            perRowStats2.fill(0)
            let skipSnapshots = branchFilters.size != 0
            points = processWindow(t0,tE,z,z2,perTableStats,perTableStats2,perRowStats,perRowStats2,1,skipSnapshots);
            console.log('processWindow case 1')
        } else {
            // some overlap, partially regenerate
            z = prevZ
            z2 = prevZ2
            perTableStats = prevPerTableStats
            perTableStats2 = prevPerTableStats2
            perRowStats = prevPerRowStats
            perRowStats2 = prevPerRowStats2

            // scale up mean aliasing to sum(aliasing) so it can be modified
            for (var i=0; i < perTableStats.length; i++){
                perTableStats[i] *= xdim; // number of rows
                perTableStats2[i] *= xdim; // number of rows
            }
            for (var i=0; i < perRowStats.length; i++){
                perRowStats[i] *= ydim; // number of tables
                perRowStats2[i] *= ydim; // number of tables
            }

            if (t0 < prevWinStart) {
                // add
                points += processWindow(t0,prevWinStart,z,z2,perTableStats,perTableStats2,perRowStats,perRowStats2,1);
                console.log('processWindow case 2')
            } else if (t0 >  prevWinStart) {
                // subtract
                points -= processWindow(prevWinStart,t0,z,z2,perTableStats,perTableStats2,perRowStats,perRowStats2,-1);
                console.log('processWindow case 3')
            }
            if (tE > prevWinEnd) {
                // add
                points += processWindow(prevWinEnd,tE,z,z2,perTableStats,perTableStats2,perRowStats,perRowStats2,1);
                console.log('processWindow case 4')
            } else if (tE < prevWinEnd) {
                // subtract
                points -= processWindow(tE,prevWinEnd,z,z2,perTableStats,perTableStats2,perRowStats,perRowStats2,-1);
                console.log('processWindow case 5')
            }
        }

        // TODO: Dividing these here and then taking the average in blocks below is not right
        for (var i=0; i < perTableStats.length; i++){
            perTableStats[i] /= xdim; // number of rows
            perTableStats2[i] /= xdim; // number of rows
        }
        for (var i=0; i < perRowStats.length; i++){
            perRowStats[i] /= ydim; // number of tables
            perRowStats2[i] /= ydim; // number of tables
        }

        // Calc new z by combining z,z2 (same with perTableStats)
        let out = zeroZ()
        let perTableStatsOut = Array(ydim);
        perTableStatsOut.fill(0)
        let perRowStatsOut = Array(xdim);
        perRowStatsOut.fill(0)
        if (statId == 'weight_change_thrash') { // weight_change_thrash
            // This should be sum(abs weight changes) / |sum(signed weight change)|
            // but *2 is not absolute. So apply abs to all final data sets
            zmatDivAbs(z,z2,out)
            yvecDivAbs(perTableStats, perTableStats2, perTableStatsOut)
            xvecDivAbs(perRowStats, perRowStats2, perRowStatsOut)
        } else if (statId == 'weight_accuracy' || statId == 'mp_harm_rate' || statId == 'mp_help_rate') {
            zmatDiv(z,z2,out)
            yvecDiv(perTableStats, perTableStats2, perTableStatsOut)
            xvecDiv(perRowStats, perRowStats2, perRowStatsOut)
        } else {
            zmatCopy(z,out)
            yvecCopy(perTableStats,perTableStatsOut)
            xvecCopy(perRowStats,perRowStatsOut)
        }

        prevWinStart = t0;
        prevWinEnd = tE;
        prevZ = z
        prevZ2 = z2
        prevPerTableStats = perTableStats
        prevPerTableStats2 = perTableStats2
        prevPerRowStats = perRowStats
        prevPerRowStats2 = perRowStats2

        // Update the heatmap distribution map with the largest n values
        let max = Array()
        let maxx = Array()
        let maxy = Array()
        let minValue = NaN
        let numToShow = 5
        for(const y of yValues) {
            zy = out[y]
            for (const x of xValues) {
                let v = zy[x]
                if (!isNaN(v)) {
                    if (isNaN(minValue) || v < minValue) {
                        minValue = v
                    }

                    if (max.length == 0 || v > max[max.length - 1]) {
                        max.push(v)
                        maxx.push(x)
                        maxy.push(y)
                        if (max.length > numToShow) {
                            max = max.slice(1,numToShow+1) // drop smallest
                            maxx = maxx.slice(1,numToShow+1)
                            maxy = maxy.slice(1,numToShow+1)
                        }
                    } else if (v > max[0]) {
                        // place in max where max[0] is smallest, max[numToShow-1] is largest
                        for (let i = 1; i < max.length; i++) {
                            if (v < max[i]) {
                                if (max.length == numToShow) {
                                    // Shift earlier down
                                    for (let j = 0; j < i-1; j++) {
                                        max[j] = max[j+1]
                                        maxx[j] = maxx[j+1]
                                        maxy[j] = maxy[j+1]
                                    }
                                    // insert
                                    max[i-1] = v
                                    maxx[i-1] = x
                                    maxy[i-1] = y
                                } else {
                                    // Push rest forward
                                    max.push(max[max.length-1]) // last to last+1
                                    maxx.push(maxx[maxx.length-1])
                                    maxy.push(maxy[maxy.length-1])
                                    for (let j = max.length-2; j >= i; j--) {
                                        max[j] =  max[j-1]
                                        maxx[j] =  maxx[j-1]
                                        maxy[j] =  maxy[j-1]
                                    }
                                    // insert
                                    max[i] = v
                                    maxx[i] = x
                                    maxy[i] = y
                                }
                                break
                            }
                        }
                    } else if (max.length < numToShow) {
                        max.splice(0,0,v)
                        maxx.splice(0,0,x)
                        maxy.splice(0,0,y)
                    }
                }
            }
        }

        let tableStr = ""
        for (let i = max.length - 1; i >= 0; i--) {
            const x = maxx[i]
            const y = maxy[i]
            const z = max[i]

            const tableIndex = Math.floor(y / numBanks)
            const bank = y % numBanks
            const bgColor = currentHeatmapColor(z)

            tableStr += '<tr><td><a href="javascript:void(0);" onclick="addTrackedCell('+y+','+x+');">[+]</a></td>' + 
                        '<td><a href="javascript:void(0);" onclick="renderRowAccessProfile('+tableIndex+','+x+','+bank+');">[prof]</a></td>' + 
                        '<td>' + y + '</td>' + 
                        '<td>' + x + '</td>' + 
                        '<td>' + Number(tableIndex).toFixed(3) + '</td>' + 
                        '<td style="background-color:' + bgColor + '";>' + z + '</td></tr>' 
        }
        $('#top-cells-tbody')[0].innerHTML = tableStr


        // Update the threshold slider based on the values seen in the heatmap
        let maxValue = null // may be inf
        for (let i = max.length - 1; i >= 0; i--) {
            if (isFinite(max[i])) {
                maxValue = max[i]
                break
            }
        }
        if (maxValue != null) {
            updateThresholdSliderLimits(minValue, maxValue, needResetThresholdSlider)
            needResetThresholdSlider = false
            $("#slider-threshold").slider("enable")
        } else {
            $("#slider-threshold").slider("disable")
        }


        // Apply thresholding to out
        // Skip if no thresholding (for performance)
        // Only apply if threshold reset is not needed.
        // Threshold reset happens when we don't know the extents yet because its the first rendering
        // WARNING: this is another pass through the heatmap.
        if (!needResetThresholdSlider) { // only apply if a reset is not needed
            let range = getThresholdSliderRange()
            let extents = getThresholdSliderExtents()
            let threshMin = range[0]
            let threshMax = range[1]

            minValue = threshMin
            maxValue = threshMax

            // Only apply if extents have changed
            if (threshMin != extents[0] || threshMax != extents[1]) {
                for(const y of yValues) {
                    zy = out[y]
                    for (const x of xValues) {
                        let v = zy[x]
                        if (v < threshMin) {
                            if (isFinite(v)) {
                                zy[x] = NaN // hide from output
                            }
                        } else if (v > threshMax)  {
                            zy[x] = NaN // hide from output
                        }
                    }
                }
            }
        }

        if (!heatmapLockZ) {
            heatmapZMin = minValue
            heatmapZMax = maxValue
        }

        lastZOut = out
        updateTrackedCellsTable()
        dataChangedSinceRegenerate = false // generated z data reflects new data

        return [out,perTableStatsOut,perRowStatsOut,points];
    }

    // Divide a/b and place result in `out`.
    function zmatDiv(a,b,out) {
        let max = NaN
        for(const y of yValues) {
            ay = a[y]
            by = b[y]
            oy = out[y]
            for (const x of xValues) {
                oy[x] = ay[x] / by[x] // NOTE: may result in NaN
                //if (isFinite(oy[x]) && !isNaN(oy[x]) && oy[x] > max) { // store max finite value
                //    max = oy[x]
                //}
            }
        }
        return max
    }
    function zmatDivAbs(a,b,out) {
        let max = NaN
        for(const y of yValues) {
            ay = a[y]
            by = b[y]
            oy = out[y]
            for (const x of xValues) {
                oy[x] = Math.abs(ay[x] / by[x]) // NOTE: may result in NaN
                //if (isFinite(oy[x]) && !isNaN(oy[x]) && oy[x] > max) { // store max finite value
                //    max = oy[x]
                //}
            }
        }
        return max
    }
    function zmatCopy(a,out) {
        let max = NaN
        for(const y of yValues) {
            ay = a[y]
            oy = out[y]
            for (const x of xValues) {
                oy[x] = ay[x]
                //if (isFinite(oy[x]) && !isNaN(oy[x]) && oy[x] > max) { // store max finite value
                //    max = oy[x]
                //}
            }
        }
        return max
    }

    function yvecDiv(a,b,out) {
        for (const y of yValues) {
            out[y] = a[y] / b[y] // NOTE: may result in Nan
        }
    }
    function yvecDivAbs(a,b,out) {
        for (const y of yValues) {
            out[y] = Math.abs(a[y] / b[y]) // NOTE: may result in Nan
        }
    }
    function yvecCopy(a,out) {
        for (const y of yValues) {
            out[y] = a[y]
        }
    }

    function xvecDiv(a,b,out) {
        for (const x of xValues) {
            out[x] = a[x] / b[x] // NOTE: may result in Nan
        }
    }
    function xvecDivAbs(a,b,out) {
        for (const x of xValues) {
            out[x] = Math.abs(a[x] / b[x]) // NOTE: may result in Nan
        }
    }
    function xvecCopy(a,out) {
        for (const x of xValues) {
            out[x] = a[x]
        }
    }

    // Iterate data in window and add counters (multiplied by op) to the z and perTableStats arrays
    function processWindow(t0, tE, z, z2, perTableStats, perTableStats2, perRowStats, perRowStats2, op, skipSnapshots=false) {
        // WARNING: assumes 0 or 1 event per cycle.
        let snapSum = 0
        let pts = 0
        let snaps = 0
        for (var ti = tE-1; ti >= t0;) {
            var tv = ts[ti];
            if(ti < 0) throw 'negative ti';
            if (ti >= ts.length) throw 'oob ti;'
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1)
            if (isSnapshot) {
                if (skipSnapshots) {
                    ti-- 
                    continue
                }

                snapStart = tv[1]
                snapEnd = tv[2]
                snapZ = tv[3]
                snapCount = tv[4]
                let snapZ2 = null
                if (tv.length >5)
                    snapZ2 = tv[5]

                if (snapStart >= t0) {
                    // Add this to bucket
                    if (ti == snapStart) {
                        // empty snapshot?
                        ti = -1;
                    } else
                        ti = snapStart;

                    snapZi = 0;
                    for(const y of yValues) {
                        zy = z[y]
                        z2y = z2[y]
                        for (const x of xValues) {
                            let v = snapZ[snapZi]
                            if (isNaN(v))
                                console.error('x=' + x + ' y=' + y + ' v=' + v)
                            let d = op*v
                            zy[x] += d
                            perTableStats[y] += d
                            perRowStats[x] += d

                            if (snapZ2 != null) {
                                let v2 = snapZ2[snapZi]
                                if (isNaN(v2))
                                    console.error('x=' + x + ' y=' + y + ' v=' + v2)
                                let d2 = op*v2
                                z2y[x] += d2
                                perTableStats2[y] += d2
                                perRowStats2[x] += d2
                            }

                            snapZi++
                            
                            // TEMP: only tracking one value for now
                            //values = snapZ[y*xdim + x];
                            //if (values == 0) {
                            //  // no data for this entry
                            //} else {f
                            //  v = values[0];
                            //  if (!Number.isInteger(v)) throw 'error:' + v + ' ' + x + ' ' + y + ' ' + values;
                            //  zy[x] += v; // TODO: all counters
                            //  perTableStats[y] += v;
                            //}
                        }
                    }
                    snaps++;
                    snapSum += snapCount
                } else {
                    ti--;
                }
            }
            else {
                // event
                let timestamp = tv[1]
                let x = tv[2];
                let y = tv[3];
                if (!Number.isInteger(x))
                    console.error('x=' + x)
                if (!Number.isInteger(y))
                    console.error('y=' + y)
                if (y > ydim)
                    console.error('y=' + y)
                if (x > xdim)
                    console.error('x=' + x)

                let c1 = tv[5];
                if (isNaN(c1))
                    console.error('c1=' + c1)

                let include = true
                if (tv.length > 6 && branchFilters.size > 0) {
                    let pcLo = tv[7]
                    let pcHi = tv[8]
                    let pcStr = renderBranchAddress(pcLo, pcHi)
                    include = branchFilters.has(pcStr)
                }

                if (include) {
                    let d = op*c1
                    z[y][x] += d; // TODO: all counters
                    perTableStats[y] += d;
                    perRowStats[x] += d
                    if (tv.length > 6) {
                        let c2 = tv[6]
                        if (isNaN(c2))
                            console.error('c2=' + c2)
                        let d2 = op*c2
                        z2[y][x] += d2; // TODO: all counters
                        perTableStats2[y] += d2;
                        perRowStats2[x] += d2
                    }
                }
                pts++;
                ti--;
            }
        }

        logDebug(function(){return 'processWindow ' + op + ' (' + t0 + '-' + tE + ') => ' + pts + ' pts, ' + snaps + ' snaps';});
        return pts + snapSum

    };

    var datarevision = 0; // counter to force redraw when data arrays are updated for plotly. Number doesn't matter as long as it changes each draw

    // Render the heatmap
    function render(zValues, perTableStats, perRowStats) {
        rowDataLayout['title'] = statId

        datarevision++; // sometimes this uses all the same data arrays with updated values

        let xArray = xValues
        if (downsampleHeatmap) {
            // Shrink x, y, and z
            // NOTE: It would be more efficient to do this in RegenerateZ, but this is easier
            const amount = 4
            const z2 = [];
            for (let i = 0; i < ydim; i++) {
                z2.push([])
                const zy = z2[z2.length -1]
                for (let j = 0; j < xdim; j+=amount) { // compress along X using max value
                    let a = []
                    for (let k = 0; k < amount; k++) {
                        const v = zValues[i][j+k]
                        if (!isNaN(v))
                            a.push(v)
                    }
                    if (a.length == 0)
                        zy.push(NaN)
                    else
                        zy.push(Math.max(...a))
                }
            }

            xArray = xArray.slice(0, Math.ceil(xArray.length/amount))
            zValues = z2
        }


        var data = [{
          x: xArray,
          y: yValues,
          z: zValues,
          type: 'heatmap', //'heatmapgl' causes blurring of cells and does not support ygap. use 'heatmap'
          colorscale: colorScaleGradient,
          ygap: 1,
          // xgap: 1, // miss things when zoomed out fully. Looks great when zoomed in though.
          //showscale: false
        }];

        if (heatmapLockZ) {
            data[0]['zauto'] = false
            data[0]['zmin'] = heatmapZMin
            data[0]['zmax'] = heatmapZMax
        }

        heatmapLayout['datarevision'] = datarevision;
        Plotly.react('heatmapDiv', data, heatmapLayout, {showSendToCloud: false});

        // TODO: only add this listener once... not every render!
        $('#heatmapDiv')[0].on('plotly_click', function(data) {
            if (data.points.length == 0)
                return

            let pt = data.points[0]
            let rowx = pt.x
            let tabley = pt.y

            addTrackedCell(tabley, rowx)
        });

        var tableData = {
            x: perTableStats,
            y: yValues,
            type: 'line',
            colorscale: colorScaleGradient
        }

        var dummyData = {
            x: [0],
            y: [0],
            type: 'markers'
        }


        // TODO: these should be subplots
        tableDataLayout['datarevision'] = datarevision;
        Plotly.react('perTableChartDiv', [tableData, dummyData], tableDataLayout, {showSendToCloud: false});

        var tableData = {
            x: xValues,
            y: perRowStats,
            type: 'scattergl', //type: 'bar' is intense to draw
            mode: 'lines',
            //marker: {
            //    size:4,
            //    colorscale: colorScaleGradient,
            //}
            line: {
                width: 0.5,
            }
        }

        // TODO: these should be subplots
        rowDataLayout['datarevision'] = datarevision;
        Plotly.react('perRowChartDiv', [tableData], rowDataLayout, {showSendToCloud: false});
    }

    var rangeTimeEl = null;

    function updateHeatmapFromSlider(forceRecalc=true) {
        var el = document.getElementById("my_range");
        vals = el.value.split(';');
        from = Number(vals[0]);
        to = Number(vals[1]);
        updateHeatmap(to, to - from, forceRecalc);
    }

    function updateHeatmap(bi, windowSize, forceRecalc=false) {
        $('#window_size')[0].value = windowSize;

        t0 = performance.now();
        var result = regenerateZ(bi, windowSize, forceRecalc);
        z = result[0]
        perTableStats = result[1]
        perRowStats = result[2]
        pts = result[3]
        t1 = performance.now();
        render(z, perTableStats, perRowStats);
        t2 = performance.now();
        logDebug(function(){return 'updated w/ brn=' + bi + ' win=' + windowSize + ' collect:' + (t1-t0) + ' render:' + (t2-t1) + ' pts:' + pts;});
    }

    // Generate the timeline graph of the selected statistic `statId`
    // Iterates the data to produce it so call this sparingly
    function renderTimeline() {
        var timelineDataLayout = {
            showlegend: true,
            title: '',
            height: 150,
            margin: { t:0, 
                      l:40, // roughly match td to the left of textbox
                      r:120, // roughly match window_size textbox
                      b:30},
            xaxis: {
                autorange: false,
                range: [tStart,tEnd],
                title: 'mean ' + statId,
                ticks: '',
                side: 'bottom'
            },
            yaxis: {
                autorange: true,
                title: 'table index',
                ticks: '',
                ticksuffix: ' ',
                autosize: true
            }
        };

        let xs = [];
        let ys = [];
        let yPerTable = [...Array(ydim)]; // ydim is just num tables, which was 'y' in heatmap but is something else here
        yPerTable = yPerTable.map(function(o) { return [];});
        let yPerTableBucketSum = Array(ydim);
        yPerTableBucketSum.fill(0);
        let yPerTableBucketCount = Array(ydim);
        yPerTableBucketCount.fill(0);
        let bucketCount = 0;
        let bucketSum = 0;
        let bucketSize = 4999; // Just trying not to collide with snapshots
        if (tEnd - tStart < 5000) {
            bucketSize = 1
        } else if (tEnd - tStart < 100000) { 
            bucketSize = Math.floor((tEnd - tStart) / 1000)
        }
        for (let i = tStart; i < tEnd; i++) {
            var tv = ts[i];
            // WARNING:  this ignores timestamps for the moment
            var isSnapshot = Number(tv[0] == 1);
            if (!isSnapshot) {
                var x = tv[2];
                var y = tv[3];
                var c1 = tv[5];
                bucketCount += 1;
                bucketSum += c1;
                
                yPerTableBucketCount[y] += 1;
                yPerTableBucketSum[y] += c1;
            }

            if ((i % bucketSize) == 0) {
                xs.push(i); // i is currently a proxy for t in this prototype
                if (bucketCount == 0) {
                    ys.push(0);
                } else {
                    ys.push(bucketSum/bucketCount);
                }
                bucketCount = 0;
                bucketSum = 0;

                for (let j = 0; j < yPerTable.length; j++) {
                    if (yPerTableBucketCount[j] == 0)
                        yPerTable[j].push(0);
                    else
                        yPerTable[j].push(yPerTableBucketSum[j]/yPerTableBucketCount[j]);
                    yPerTableBucketSum[j] = 0
                    yPerTableBucketCount[j] = 0;
                }
            }
        }

        var timelineData = []
        let lineWidth = 0.4 // thickness of lines (<1 implies transparency)
        for(let tbl=0; tbl < ydim; tbl++) {
            timelineData.push({name: 'Table ' + tbl, x: xs, y: yPerTable[tbl], type: 'lines', line: { width:lineWidth }});
        };
        timelineData.push({
            name: 'Mean',
            x: xs,
            y: ys,
            type: 'line',
        });

        Plotly.react('timelineDiv', timelineData, timelineDataLayout, {showSendToCloud: false});
    }


    function Bucket(name, scale=1) {
        this.name = name
        this.scale = scale
        this.count = 0
        this.sum = 0
        this.array = []

        this.add = function(v) {
            this.sum += v
            this.count++
        }

        this.nextBucket = function() {
            if (this.count == 0) {
                this.array.push(0);
            } else {
                this.array.push(this.scale*this.sum/this.count);
            }
            this.count = 0;
            this.sum = 0;
        }

        this.plotlyTrace = function(template) {
            let copy = Object.assign({}, template)
            copy['name'] = this.name
            copy['y'] = this.array
            return copy
        }
    }

    function renderMispredictSummary() {
        $('#selection-stats-stale').html('') // no longer stale

        const mpBucket = new Bucket('mispred%', 100)
        const youtBucket = new Bucket('yout')
        const takenBucket = new Bucket('taken%', 100)
        const thetaBucket = new Bucket('theta@train')
        const biasBucket = new Bucket('bias@traini')
        const shpQWFBucket = new Bucket('shpq found%', 100)
        const dynamicATBucket = new Bucket('dyn_AT%', 100)
        const dynamicANTBucket = new Bucket('dyn_ANT%', 100)
        const dynamicTNTBucket = new Bucket('dyn_TNT%', 100)
        const indirectBucket = new Bucket('indirect%', 100)
        const uncondBucket = new Bucket('uncond%', 100)

        let slider = $('.js-range-slider').data('ionRangeSlider')
        const bE = slider.old_to
        const bS = slider.old_from;

        // Full iteration through data
        for (let b = bS; b < bE; b++) {
            var tv = bs[b];

            // Branch training trace
            mpBucket.add(!tv[2])
            youtBucket.add(tv[4])
            takenBucket.add(tv[3])
            thetaBucket.add(tv[5])
            biasBucket.add(tv[6])
            shpQWFBucket.add(tv[7]) // shpq_weights_found
            dynamicATBucket.add(tv[8] == 1) // dynamic_state
            dynamicANTBucket.add(tv[8] == 2) // dynamic_state
            dynamicTNTBucket.add(tv[8] == 3) // dynamic_state
            indirectBucket.add(tv[9]) // indirect
            uncondBucket.add(tv[10]) // uncond
        }

        mpBucket.nextBucket()
        youtBucket.nextBucket()
        takenBucket.nextBucket()
        thetaBucket.nextBucket()
        biasBucket.nextBucket()
        shpQWFBucket.nextBucket()
        dynamicATBucket.nextBucket()
        dynamicANTBucket.nextBucket()
        dynamicTNTBucket.nextBucket()
        indirectBucket.nextBucket()
        uncondBucket.nextBucket()

        const thead = $('#selection-stats-thead')
        let theadStr = "<tr style='background-color:#c0c0f0;'>" +
                       "    <th>name</th>" +
                       "    <th>value</th>" +
                       "</tr>"
        thead.html(theadStr)

        const tbody = $('#selection-stats-tbody')
        let tbodyStr = "<tr><td style='padding-right:4px;'>" + mpBucket.name + "</td><td style='text-align:left;'>" + mpBucket.array[0] + "</td></tr>" +
                       "<tr><td style='padding-right:4px;'>" + youtBucket.name + "</td><td style='text-align:left;'>" + youtBucket.array[0] + "</td></tr>" +
                       "<tr><td style='padding-right:4px;'>" + takenBucket.name + "</td><td style='text-align:left;'>" + takenBucket.array[0] + "</td></tr>" +
                       "<tr><td style='padding-right:4px;'>" + thetaBucket.name + "</td><td style='text-align:left;'>" + thetaBucket.array[0] + "</td></tr>" +
                       "<tr><td style='padding-right:4px;'>" + biasBucket.name + "</td><td style='text-align:left;'>" + biasBucket.array[0] + "</td></tr>" +
                       "<tr><td style='padding-right:4px;'>" + shpQWFBucket.name + "</td><td style='text-align:left;'>" + shpQWFBucket.array[0] + "</td></tr>" +
                       "<tr><td style='padding-right:4px;'>" + dynamicATBucket.name + "</td><td style='text-align:left;'>" + dynamicATBucket.array[0] + "</td></tr>" +
                       "<tr><td style='padding-right:4px;'>" + dynamicANTBucket.name + "</td><td style='text-align:left;'>" + dynamicANTBucket.array[0] + "</td></tr>" +
                       "<tr><td style='padding-right:4px;'>" + dynamicTNTBucket.name + "</td><td style='text-align:left;'>" + dynamicTNTBucket.array[0] + "</td></tr>" +
                       "<tr><td style='padding-right:4px;'>" + indirectBucket.name + "</td><td style='text-align:left;'>" + indirectBucket.array[0] + "</td></tr>" +
                       "<tr><td style='padding-right:4px;'>" + uncondBucket.name + "</td><td style='text-align:left;'>" + uncondBucket.array[0] + "</td></tr>"
        tbody.html(tbodyStr)

    }

    // Generate the mispredict (and other stats) timeline graph
    // Iterates the data to produce it so call this sparingly.
    // This does not change based on selected stat, so this should only need to be
    function renderMispredictRate(bucketSize = 5000) {
        var mpTimelineDataLayout = {
            showlegend: true,
            title: '',
            height: 150,
            margin: { t:0, 
                      l:40, // roughly match td to the left of textbox
                      r:120, // roughly match window_size textbox
                      b:30},
            xaxis: {
                autorange: false,
                range: [0, bs.length],
                title: 'mispredict rate',
                ticks: '',
                side: 'bottom'
            },
            yaxis: {
                autorange: true,
                title: 'table index',
                ticks: '',
                ticksuffix: ' ',
                autosize: true
            }
        };

        let xs = [];
        let ys = [];

        const mpBucket = new Bucket('mispred%', 100)
        const youtBucket = new Bucket('yout')
        const takenBucket = new Bucket('taken%', 100)
        const thetaBucket = new Bucket('theta@train')
        const biasBucket = new Bucket('bias@traini')
        const shpQWFBucket = new Bucket('shpq found%', 100)
        const dynamicATBucket = new Bucket('dyn_AT%', 100)
        const dynamicANTBucket = new Bucket('dyn_ANT%', 100)
        const dynamicTNTBucket = new Bucket('dyn_TNT%', 100)
        const indirectBucket = new Bucket('indirect%', 100)
        const uncondBucket = new Bucket('uncond%', 100)

        if (tEnd - tStart < 5000) {
            bucketSize = 1
        } else if (tEnd - tStart < 100000) { 
            bucketSize = Math.floor((tEnd - tStart) / 1000)
        }
        for (let i = 0; i < bs.length; i++) {
            var tv = bs[i];
            // WARNING:  this ignores timestamps 

            // Branch training trace
            mpBucket.add(!tv[2])
            youtBucket.add(tv[4])
            takenBucket.add(tv[3])
            thetaBucket.add(tv[5])
            biasBucket.add(tv[6])
            shpQWFBucket.add(tv[7]) // shpq_weights_found
            dynamicATBucket.add(tv[8] == 1) // dynamic_state
            dynamicANTBucket.add(tv[8] == 2) // dynamic_state
            dynamicTNTBucket.add(tv[8] == 3) // dynamic_state
            indirectBucket.add(tv[9]) // indirect
            uncondBucket.add(tv[10]) // uncond

            if ((i % bucketSize) == 0) {
                xs.push(i); // i is currently a proxy for t in this prototype
                mpBucket.nextBucket()
                youtBucket.nextBucket()
                takenBucket.nextBucket()
                thetaBucket.nextBucket()
                biasBucket.nextBucket()
                shpQWFBucket.nextBucket()
                dynamicATBucket.nextBucket()
                dynamicANTBucket.nextBucket()
                dynamicTNTBucket.nextBucket()
                indirectBucket.nextBucket()
                uncondBucket.nextBucket()
            }
        }

        var mpData = []
        let lineWidth = 0.6 // thickness of lines (<1 implies transparency)
        const template = {
                name: 'Mean',
                x: xs,
                y: null,
                type: 'line',
                line: {
                    width: lineWidth,
                }
            }

        mpData.push(mpBucket.plotlyTrace(template))
        mpData.push(youtBucket.plotlyTrace(template))
        mpData.push(takenBucket.plotlyTrace(template))
        mpData.push(thetaBucket.plotlyTrace(template))
        mpData.push(biasBucket.plotlyTrace(template))
        mpData.push(shpQWFBucket.plotlyTrace(template))
        mpData.push(dynamicATBucket.plotlyTrace(template))
        mpData.push(dynamicANTBucket.plotlyTrace(template))
        mpData.push(dynamicTNTBucket.plotlyTrace(template))
        mpData.push(indirectBucket.plotlyTrace(template))
        mpData.push(uncondBucket.plotlyTrace(template))
        
        //mpData.push({
        //    name: 'Mean',
        //    x: xs,
        //    y: mpBucket.array,
        //    name: 'Mispredict Rate'
        //    type: 'line',
        //    line: {
        //        width: lineWidth,
        //    }
        //});

        Plotly.react('mpDiv', mpData, mpTimelineDataLayout, {showSendToCloud: false});
    }

    // Upon loading new data, perform some updates and redraws
    function doPostDataLoad() {
        renderMispredictRate();
        renderTimeline();
        logDebug(function(){return 'generating z for window...';});
        setTimeout(function() {
            needResetThresholdSlider = true // need to reset threshold slider for new data
            updateHeatmapFromSlider()

            $('#selectionStatTable')[0].style.visibility = 'visible';
            $('#branchHistoryTable')[0].style.visibility = 'visible';
            $('#branchImemTable')[0].style.visibility = 'visible';
            $('#topCellsTable')[0].style.visibility = 'visible';
            $('#trackedCellsTable')[0].style.visibility = 'visible';
            $('#hm-row-profile-table')[0].style.visibility = 'visible';
        }, 1);
    }

    function markProfilesAsStale() {
        const s = '(STALE)'
        $('#branch-imem-stale').html(s)
        $('#branch-history-stale').html(s)
        $('#branch-write-timeline-stale').html(s)
        $('#hm-row-profile-stale').html(s)
        $('#selection-stats-stale').html(s)
    }

    function initRangeSlider(minval, maxval) {
        let range = maxval - minval
        $(".js-range-slider").ionRangeSlider({
            type: 'double',
            skin: 'big', // 'round',
            grid: true,
            grid_snap: false,
            //grid_cells: 10,
            grid_num: 10,
            min: bStart,
            max: bEnd,
            from: Math.max(0, tEnd - Math.min(range, 600000)),
            to: tEnd,
            drag_interval: true,
            min_interval: 100,
            max_interval: null,
            onChange: function (data) {
                if (ts != null) // ensure data is ready
                    updateHeatmap(data.to, data.to-data.from, false /* no force recalc */);
            },
            onUpdate: function (data) {
                if (ts != null) // ensure data is ready
                    updateHeatmap(data.to, data.to-data.from, false /* no force recalc */);
            }

        });
        $( ".widget input[type=submit], .widget a, .widget button" ).button();
        $(".js-range-slider").click( function (event) { // stop this slider from handling html 'input' element clicks
          event.preventDefault();
        } );
        jQuery('.integer-text').keyup(function () { 
        this.value = this.value.replace(/[^0-9]/g,'');
        });
        jQuery('.float-text').keyup(function () { 
            this.value = this.value.replace(/[^0-9\.]/g,'');
        });
    }

    function initThresholdSlider() {
        let handle = $( "#slider-threshold-handle" );
        let handle2 = $( "#slider-threshold-handle2" );
        $( "#slider-threshold" ).slider({
          orientation: "vertical",

          range: true,
          step: 0.01,
          min: 0,
          max: 100,
          values: [0,100],
          slide: function (event, ui) {
            let lo = ui.values[0] // $( "#slider-threshold" ).slider( "values", 0 )
            let hi = ui.values[1] // $( "#slider-threshold" ).slider( "values", 1 )
            
            handle.text(lo)
            handle2.text(hi)

            updateThresholdValueDisplay(lo, hi)
            updateHeatmapFromSlider(forceRecalc=false) // reapply threshold to heatmap
          }



          //range: "max",
          //min: 0,
          //max: 100,
          //value: 0,
          //create: function() {
          //      handle.text($(this).slider("value"))
          //},
          //slide: function( event, ui ) {
          //  //$( "#thresh-range" ).val(ui.value);
          //  handle.text($(this).slider("value"))
          //  updateThresholdValueDisplay()
          //  updateHeatmapFromSlider() // reapply threshold
          //}

          //range: true,
          //values: [ 17, 67 ],
          //slide: function( event, ui ) {
          //  $( "#thresh-range" ).val( "$" + ui.values[ 0 ] + " - $" + ui.values[ 1 ] );
          //}
        });
        //updateThresholdValueDisplay()
        //$( "#thresh-range" ).val( "$" + $( "#slider-threshold" ).slider( "values", 0 ) +
        //  " - $" + $( "#slider-threshold" ).slider( "values", 1 ) );

        let lo = $( "#slider-threshold" ).slider( "values", 0 )
        let hi = $( "#slider-threshold" ).slider( "values", 1 )
        updateThresholdValueDisplay(lo, hi)
        handle.text(lo)
        handle2.text(hi)
    }

    function setSliderThresholdHighToMax() {
        let max = $( "#slider-threshold" ).slider("option", "max")
        $( "#slider-threshold" ).slider( "values", 1, max)

        let lo = $( "#slider-threshold" ).slider( "values", 0 )
        updateThresholdValueDisplay(lo, max)

        let handle = $( "#slider-threshold-handle" );
        let handle2 = $( "#slider-threshold-handle2" );
        handle.text(lo)
        handle2.text(max)

        updateHeatmapFromSlider()
    }

    function setSliderThresholdLowToMin() {
        let min = $( "#slider-threshold" ).slider("option", "min")
        $( "#slider-threshold" ).slider( "values", 0, min)

        let hi = $( "#slider-threshold" ).slider( "values", 1 )
        updateThresholdValueDisplay(min, hi)

        let handle = $( "#slider-threshold-handle" );
        let handle2 = $( "#slider-threshold-handle2" );
        handle.text(min)
        handle2.text(hi)

        updateHeatmapFromSlider()
    }

    // Upddate the max value (i.e. range) of the threshold slider based on new data
    function updateThresholdSliderLimits(newMin,newMax,resetRange=false) {
        let curMax = $( "#slider-threshold" ).slider("option", "max")
        let curMin = $( "#slider-threshold" ).slider("option", "min")

        let range = getThresholdSliderRange()
        let curLow = range[0]
        let curHigh = range[1]

        // Update max and min
        $( "#slider-threshold" ).slider("option", "max", newMax)
        $( "#slider-threshold" ).slider("option", "min", newMin)

        if (curHigh == curMax || resetRange) {
            curHigh = newMax
            $("#slider-threshold").slider("values", 1, newMax) // follow newMax whether up or down
        }
        else if (curHigh > newMax) {
            $("#slider-threshold").slider("values", 1, newMax) // only follow new newMax if smaller
        }

        if (curLow == curMin || resetRange) {
            curLow = newMin
            $("#slider-threshold").slider("values", 0, newMin) // follow newMin whether up or down
        }
        else if (curLow < newMin) {
            $("#slider-threshold").slider("values", 0, newMin) // only follow newMin if larger
        }

        if (curLow > curHigh) {
            // Expand the range in whichever direction has any room
            curLow = curHigh
            $("#slider-threshold").slider("values", 0, curLow)
        }
        else if (curLow == curHigh) {
            // this is OK, because the range is inclusive.
        }

        let handle = $( "#slider-threshold-handle" );
        let handle2 = $( "#slider-threshold-handle2" );
        handle.text(curLow)
        handle2.text(curHigh)

    }

    function disableHeatmapDownsample() {
        downsampleHeatmap = false
        updateHeatmapFromSlider()
    }

    function enableHeatmapDownsample() {
        downsampleHeatmap = true
        updateHeatmapFromSlider()
    }

    function unlockHeatmapExtents() {
        heatmapLockZ = false
        updateHeatmapFromSlider()
    }

    function lockHeatmapExtents() {
        // No need to redraw, just lock in current values for heatmapZMin/Max
        // In practice this causes a tiny jitter on the next redraw
        heatmapLockZ = true 
    }

    function updateThresholdValueDisplay(min, max) {
        $("#thresh-range").val('[' + min + ',');
        $("#thresh-range2").val(max  +']');
    }

    function getThresholdSliderRange() {
        let lo = $( "#slider-threshold" ).slider( "values", 0 )
        let hi = $( "#slider-threshold" ).slider( "values", 1 )
        return [lo, hi]
    }

    function getThresholdSliderExtents() {
        let min = $( "#slider-threshold" ).slider( "option", "min" )
        let max = $( "#slider-threshold" ).slider( "option", "max" )
        return [min, max]   
    }


    $( function() {
        initRangeSlider(bStart,bEnd)
        initThresholdSlider()
    });


    var stopAnimationFlag = false
    function animationStep(stepSize, stepsPerSecond) {
        let slider = $('.js-range-slider').data('ionRangeSlider')

        let range = slider.old_to - slider.old_from
        let from = slider.old_from + stepSize;
        let to = slider.old_to + stepSize;

        if (to > bEnd) {
            to = bEnd
            from = to - range
        }

        slider.update({from: from, to: to});

        if (to == bEnd)
            stopAnimation();

        // TEMP:
        ////stopAnimation();
        if (!stopAnimationFlag) {
            // TODO: This does not take into account how long the animation step took to.
            //       Needs to track last frame start time.
            let delayMs = 1000.0 / stepsPerSecond;
            setTimeout(function() { animationStep(stepSize, stepsPerSecond) }, delayMs);
        }
    }

    function beginAnimation() {
        var stepSize = Number($('#cycles_per_step')[0].value);
        if (stepSize <= 0) alert('Stepsize must be > 0');
        var stepsPerSecond = Number($('#steps_per_second')[0].value);
        if (stepsPerSecond <= 0) alert ('Steps/second must be > 0');

        $('#animate')[0].style.display = 'none';
        $('#stop')[0].style.display = 'inline-block';

        let slider = $('.js-range-slider').data('ionRangeSlider')
        if (slider.old_to == bEnd) {
            slider.update({from: bStart, to: bStart + (slider.old_to - slider.old_from)});
        }

        stopAnimationFlag = false;
        animationStep(stepSize, stepsPerSecond);
    };

    function stopAnimation() {
        $('#animate')[0].style.display = 'inline-block';
        $('#stop')[0].style.display = 'none';

        stopAnimationFlag = true;
    };

    function onWindowTextEdited() {
        // user changed this
        let txt = $('#window_size')[0];
        txt.style.fontStyle = 'italic';
        txt.style.color = '#a0a0a0';
        txt.style.fontWeight = '';
    };

    function onWindowTextVerified() {
        let txt = $('#window_size')[0];
        txt.style.fontStyle = '';
        txt.style.color = '#000000';
        txt.style.fontWeight = 'bold';
    }

    function onWindowTextChange(value) {
        let r = Number(value);
        if (r <= 0) alert ('enter a valid window size value > 0, not ' + r);

        let slider = $('.js-range-slider').data('ionRangeSlider')
        let old_range = slider.old_to - slider.old_from;
        
        if (r > old_range) {
            // grow
            let grow = r - old_range;
            let new_to
            let new_from

            // inrcrease the 'to' first
            if (slider.old_to + grow > bEnd) {
                grow -= bEnd - slider.old_to;
                new_to = bEnd;
            } else {
                new_to = slider.old_to + grow;
                grow = 0;
            }

            if (grow > 0) {
                if (slider.old_from - grow < bStart) {
                    new_from = bStart;
                    grow -= slider.old_from - bStart;
                } else {
                    new_from = slider.old_from - grow;
                    grow = 0;
                }
            } else {
                new_from = slider.old_from
            }

            if (grow > 0) {
                $('#window_size')[0].value = r - grow;
            }

            // Update slider to reflect range. This will upate the heatmap
            slider.update({from: new_from, to: new_to});

        } else if (r < old_range) {
            // shrink

            // only shrink the "from" so that "current cycle" doesn't change
            let new_from = slider.old_from + (old_range - r);
            slider.update({from: new_from, to: slider.old_to});
        }
        
        onWindowTextVerified();
    };

    function onCheckChange(box) {
        // TODO:
    }

    function addBranchFilter(pcStr, idAdd, idRem) {
        branchFilters.set(pcStr, 1)
        $('#' + idAdd)[0].style.display='none';
        $('#' + idRem)[0].style.display='inline-block';
        updateHeatmapFromSlider()
        rerenderBranchHistoryListIfShown() // Apply filter to this list
    }

    function remBranchFilter(pcStr, idAdd, idRem) {
        $('#' + idAdd)[0].style.display='inline-block';
        $('#' + idRem)[0].style.display='none';

        if (!branchFilters.has(pcStr))
            return // not present.

        branchFilters.delete(pcStr)
        updateHeatmapFromSlider()
    }

    function clearBranchFilters() {
        if (branchFilters.size == 0)
            return // already empty

        branchFilters = new Map()
        updateHeatmapFromSlider()
    }

    function addTrackedCell(tabley,rowx) {
        for (let i = 0; i < trackedCells.length; i++) {
            if(trackedCells[i][0] == rowx && trackedCells[i][1] == tabley) {
                return // already present
            }
        }
        trackedCells.push([rowx,tabley])
        updateTrackedCellsTable()
    }
    function removeTrackedCell(tabley,rowx) {
        for (let i = 0; i < trackedCells.length; i++) {
            if(trackedCells[i][0] == rowx && trackedCells[i][1] == tabley) {
                trackedCells.splice(i,1) 
                break
            }
        }
        updateTrackedCellsTable()
    }

    // Repopulate the tracked cells table based on the `trackedCells` data structure
    function updateTrackedCellsTable() {

        $('#tracked-cells-thead').html(
            "<tr style='background-color:#c0c0f0;'>" + 
            "    <th>show</th>" + 
            "    <th>profile</th>" + 
            "    <th>Table (incl. bank)</th>" + 
            "    <th>Row</th>" + 
            "    <th>Bank</th>" + 
            "    <th>Value</th>" + 
            "    <th>X</th>" + 
            "</tr>"
            )

        let tableStr = ""
        for (let i = 0; i < trackedCells.length; i++) {
            const rowx = trackedCells[i][0]
            const tabley = trackedCells[i][1]
            const z = lastZOut[tabley][rowx]
            const bgColor = currentHeatmapColor(z)

            const tableIndex = Math.floor(tabley / numBanks)
            const bank = tabley % numBanks

            // TODO: onChange support?
            tableStr += '<tr>' + 
                        '<td><input type="checkbox" value="checked" />(todo)</td>' + 
                        '<td><a href="javascript:void(0);" onclick="renderRowAccessProfile('+tableIndex+','+rowx+','+bank+');">[prof]</a></td>' + 
                        '<td>' + tabley + '</td>' + 
                        '<td>' + rowx + '</td>' + 
                        '<td>' + Number(tableIndex).toFixed(3) + '</td>' + 
                        '<td style="background-color:' + bgColor + ';">' + z + '</td>' + 
                        '<td><a href="javascript:void(0);" onclick="removeTrackedCell('+tabley+','+rowx+');">[x]</a></td>' + 
                        '</tr>'
        }
        $('#tracked-cells-tbody')[0].innerHTML = tableStr
    }


    // Generate some collapsible content for the call-stack table
    // This is just an example
    function buildCallStackTable() {
        var jsTT = com_github_culmat_jsTreeTable

        ////jsTT.treeTable($('#callStack'));

        var data = [
            {
              id : 'Function A',
              inclusive : '80%',
              mp_rate_excl : ''
            }, {
              id : 'Function B',
              inclusive : '20%',
              mp_rate_excl : '.01%',
              mp_rate_excl : ''
            }, {
              id : 'Function C',
              inclusive : '30%',
              parent : 'Function A',
              mp_rate_excl : ''
            }, {
              id : 'Function D',
              inclusive : '70%',
              parent : 'Function A',
              mp_rate_excl : '.4%'
            }, {
              id : 'Function E',
              inclusive : '10%',
              parent : 'Function C',
              mp_rate_excl : '1.1%'
            }, {
              id : 'Function F',
              inclusive : '90%',
              parent : 'Function C',
              mp_rate_excl : '.4%'
            }]
                
        var tree = jsTT.makeTree(data);
        var rendered = jsTT.renderTree(tree, 'children', 'id', {'id':'Name','inclusive':'instances(inc)', 'mp_rate_excl':'mp%(self)'}, null, {'class':'callStackTable'});
        var final = jsTT.treeTable(rendered);
        //final.insertAfter($('#treetable-rendered'))
        //final.replace($('#callStackDiv'));
        ////let div = $('#callStackDiv');
        ////div[0].innerHTML = final[0].outerHTML;
        let prev = $('callStackTableElement');
        if (prev.length)
            prev.remove();
        //final.insertAfter($('#callStackDiv'))

        $('#callStackDiv')[0].innerHTML = final[0].outerHTML
    }

    // Takes a file from a FileList object's selection handler
    // Sets global currentFile
    function loadNewFile(file) {
        currentFile = file
        let version = 1 
        if (file.name.includes('.V2.')) // use filename to figure out file format version (experimenting)
            version = 2
        if (file.name.includes('.V3.')) // use filename to figure out file format version (experimenting)
            version = 3

        const acr = new AsyncChunkedReader(file)
        showBusyBox()

        // TEMP: show tiny plots instead
        loadMassagedRealData(acr, version, function() { 
            doPostDataLoad(); hideBusyBox();
        }, skipBranches=17000000, maxNumBranches=8000000)
        //quickPlot(acr, version, function() { 
        //    hideBusyBox();
        //})
    }

    function handleFileSelect(evt) {
        var files = evt.target.files; // FileList object
        if (files.length == 0)
            return

        // files is a FileList of File objects. List some properties.
        //var output = [];
        //for (var i = 0, f; f = files[i]; i++) {
        //  output.push('<strong>', escape(f.name), '</strong> (', f.type || 'n/a', ') - ',
        //              f.size, ' bytes, last modified: ',
        //              f.lastModifiedDate ? f.lastModifiedDate.toLocaleDateString() : 'n/a');
        //}
        //document.getElementById('current-filename').innerHTML = output.join('');

        let f = files[0]
        $('#current-filename').innerHTML = escape(f.name) + ' (' + f.size + ' bytes)'
        
        loadNewFile(f)
    }

    // DOM is ready to go. This is like onLoad.
    $(document).ready( function() {
        $("#file").on('change', handleFileSelect);

        // Init treeTable stuff
        var jsTT = com_github_culmat_jsTreeTable
        // register on the window object
        com_github_culmat_jsTreeTable.register(this)

        // Heatmap z-extent toggle switch
        $('#toggler-lockz').click(function(){
            $('#lock-z-scale-label').toggleClass('off')
            $(this).toggleClass('off');
            if ($(this).hasClass('off')) {
                unlockHeatmapExtents()
            } else {
                lockHeatmapExtents()
            }
        });

        $('#toggler-downsample').click(function(){
            $('#downsample-label').toggleClass('off')
            $(this).toggleClass('off');
            if ($(this).hasClass('off')) {
                disableHeatmapDownsample()
            } else {
                enableHeatmapDownsample()
            }
        });

        // Set up initial data
        dbgEl = $('#debugDiv')[0];
        setTimeout(function() {
            logDebug(function(){return 'generating random data...';});
            showBusyBox()
            setTimeout(function() {
                try {
                    generateRandomData();
                    doPostDataLoad()
                } finally {
                    hideBusyBox()
                }
            }, 1);
        }, 1);
    } );

    // User selected a new statistic (maybe) from the select box
    function selectNewStatistic(selected_options) {
        if (selected_options.length == 0)
            return

        let value = selected_options[0].value
        console.log(value)

        statId = value // assign global

        if (currentFile == null)
            return // nothing new to load

        loadNewFile(currentFile)
    }

    function showBusyBox() {
        $('#busy-div')[0].style.display = "table"
    }

    function updateBusyBox(s) {
        $('#busy-txt').html(s)
    }

    function hideBusyBox() {
        $('#busy-div')[0].style.display= "none"
    }

  </script>
</head>

<body>
  <div id="busy-div" style="z-index:100; display:none; background: rgba(200,200,200,0.9); position:absolute; width:100%; height:100%; top: 0; right: 0; bottom: 0; left: 0; text-align: center;">
    <span style="display:table-cell; vertical-align: middle;">
        <h2>Processing...</h2>
        <span id="busy-txt"></span><br/>
        <em>prototype: if this mesage persists for more than 10 seconds with under 5 million data points, check console errors and restart</em>
    </span>
  </div>
  <div class="prototype-warning">PROTOTYPE. CHROME ONLY</div>
  <div style="display:inline-block;">
    <form method="post" enctype="multipart/form-data">
        <em>Data:</em> <input type="file" id="file" />
        <output id="current-filename"></output>
        &nbsp;&nbsp;&nbsp;<em>Stat:</em><select onchange="selectNewStatistic(this.selectedOptions)">
            <option value="accesses">Write Accesses</option>
            <option value="weight_change">Weight Change</option>
            <option value="weight_change_mag">Weight Changes Absolute Sum</option>
            <option value="weight_change_thrash">Weight Thrashing</option>
            <option value="weight_change_events">Weight Update Events</option>
            <option value="thrash_1">Write Thrashing (History length of 1)</option>
            <option value="unique_pcs">Unique Write PCs (first seen within window only)</option>
            <option value="contrib_mispred">Row Weight Contributed to a SHP Mispredict</option>
            <option value="contra_mispred">Row Weight Contradicted a SHP Mispredict</option>
            <option value="mispred">Part of a Mispredict [stat not quite accurate]</option>
            <option value="mp_harm_rate">Row Weight Mispredict Contribute Rate (HARMFUL)</option>
            <option value="mp_help_rate">Row Weight Mispredict Contradict Rate (HELPFUL)</option>
            <option value="weight_mpreds">Row Weight Alone would be a Misprediction (ignore bias)</option>
            <option value="weight_cpreds">Row Weight Alone would be a Correct Prediction (ignore bias)</option>
            <option value="weight_accuracy">Row Weight Alone Prediction Accuracy (ignore bias)</option>
            <option value="">Write Thashing (History length of 2) [NOT AVAILABLE]</option>
            <option value="">Write Thashing (History length of 5) [NOT AVAILABLE]</option>
            <option value="">Write Thashing (History length of 10) [NOT AVAILABLE]</option>
            <option value="">Read Accesses [NOT AVAILABLE]</option>
        </select>
        
        <div class="vertical-middle-togglebox-div">
            <div class ="toggler-container">
                <a href="javascript:void(0);" id="toggler-downsample" class="toggler off">&nbsp;</a>
            </div>
            <div style='display:inline-block; margin-left:2px; vertical-align:middle;' id='downsample-label' class='off'>
                Downsample Heatmap:<br/>(loses fidelity)
            </div>
        </div>
        <div class="vertical-middle-togglebox-div">
            <div class ="toggler-container">
                <a href="javascript:void(0);" id="toggler-lockz" class="toggler off">&nbsp;</a>
            </div>
            <div style='display:inline-block; margin-left:2px; vertical-align:middle;' id='lock-z-scale-label' class='off'>
                Lock<br/>Z-Scale
            </div>
        </div>
    </form>
  </div>
  <div class="slidecontainer" style="width:100%;">

    <!-- upper section with heatmap -->
    <table style="width:100%" cellpadding=0 cellspacing=0>
        <tbody>
            <tr>
                <td style="width:180px; height:100px;">
                    <div style="padding:2px; border:1px solid #c0c0c0; margin-right:10px;"> 
                        <button class="ui-button ui-widget ui-corner-all" id="animate" onclick="beginAnimation();" style="width:100%; margin-bottom:2px" >Animate</button>
                        <button class="ui-button ui-widget ui-corner-all" id="stop" onclick="stopAnimation();" style="display:none; width:100%; margin-bottom:2px" >Stop</button><br/>
                        <input type="text" class="integer-text ui-widget ui-corner-all" id="cycles_per_step" value="500" style="width:50px; font-size:12px"/>
                        branches/step<br/>
                        <input type="text" class="float-text ui-widget ui-corner-all" id="steps_per_second" value="5.0" style="width:50px; font-size:12px" /> steps/second
                        <!--<span class="note">(depending on the window size, this may be verry clunky)</span><br/>-->
                    </div>
                </td>
                <td>
                    <div id="perRowChartDiv" style="width:100%;"></div>
                </td>
                <td style="width:20px;">
                    <!--
                        <label for="thresh-range">thresh</label>
                        <input type="text" id="thresh-range" readonly style="border:0; color:#f6931f; font-weight:bold; width:40px; display:none">
                        <input type="text" id="thresh-range2" readonly style="border:0; color:#f6931f; font-weight:bold; width:40px; display:none">
                    -->
                </td>
            </tr>
            <tr>
                <td style="width:180px; height:350px;">
                    <div id="perTableChartDiv"></div>
                </td>
                <td>
                    <div id="heatmapDiv"></div>
                </td>
                <td style="width:48px; height:350px;">
                    <button onclick="setSliderThresholdHighToMax()" style="margin-bottom:16px">max</button>
                    <div id="slider-threshold" style="height:250px; margin-left:2px;">
                        <div id="slider-threshold-handle" class="ui-slider-handle"></div>
                        <div id="slider-threshold-handle2" class="ui-slider-handle"></div>
                    </div>
                    <button onclick="setSliderThresholdLowToMin()" style="margin-top:8px">min</button>
                </td>
            </tr>
        </tbody>
    </table>

    <!-- lower section with graphs and slider -->
    <div style="float:left; width:70%; padding:0px; box-sizing:border-box; min-height:200px;">
        <!-- timeline graphs -->
        <div style="width:100%; height:160px; border-top:1px solid black;">
            <div id="timelineDiv"><!-- Plotly chart will be drawn inside this DIV --></div>
        </div>
        <div style="width:100%; height:160px; border-top:1px solid black;">
            <div id="mpDiv"><!-- Mispredict rate? --></div>
        </div>
        <!-- slider: inside its own table to deal with jitter on layout caused by the slider itself-->
        <table style="width:100%" cellpadding=0 cellspacing=0>
            <tbody>
                <tr>
                    <td style="width:30px;">
                    <td>
                        <input type="text" class="js-range-slider" id="my_range" value=""/>
                    </td>
                    <td style="width:100px; text-align:right;">
                        window<br/>
                        <input type="text" class="integer-text ui-widget ui-corner-all" id="window_size" value="-" style="margin-top:0px; width:80px;" onchange="onWindowTextChange(this.value);" oninput="onWindowTextEdited();" />
                    </td>
                </tr>
            </tbody>
        </table>
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Branch Trace</span><em style="font-size:12px;">(last N <u>filtered</u> branches in selection range)</em>
            <input type="button" onClick="renderBranchHistoryList(true);" value="generate(from start)" />
            <input type="button" onClick="renderBranchHistoryList(false);" value="generate(from end)" />
            <input type="button" onClick="clearBranchHistoryList();" value="clear" />
            <input type="checkbox" id="branch-history-show-no-write-branches-checkbox" onClick="rerenderBranchHistoryListIfShown();" checked><div style="display:inline-block; font-size:12px; padding-right:15px;">Show branches w/o<br/>weight updates</div>
            <input type="checkbox" id="branch-history-show-only-wrong-branches-checkbox" onClick="rerenderBranchHistoryListIfShown();"><div style="display:inline-block; font-size:12px; padding-right:15px;">Show only mispredicts</div>
            <span id="branch-history-stale" class="stale-notice"></span>
            <div style="overflow-y:scroll;">
                <table id="branchHistoryTable" style="width:100%; margin-top:10px; font-family:monospace; text-align:right;visibility:hidden">
                    <thead id="branch-history-thead">
                    </thead>
                    <tbody id="branch-history-tbody">
                    </tbody>
                </table>
            </div>
        </div>        
        <div style="border:1px solid grey; padding:10px">
            <input type="button" onClick="generateRowAccessTimeline();" value="Generate Timeline (slow)">
            <input type="button" onClick="clearRowAccessTimeline();" value="clear">
            <span id="branch-write-timeline-stale" class="stale-notice"></span>
            <div id="rowAccessTimelineDiv"></div>
        </div>
        <div style="border:1px solid grey; padding:10px">
            Should be able to mark ranges here somehow as reminders of interesting regions?
        </div>
        <div style="border:1px solid grey; padding:10px">
            An auto-zoomed view of the timeline plot should replace it. The non-zoomed timeline plot belongs here.
        </div>
        <div style="border:1px solid grey; padding:10px">
            Flame-graph? Or maybe a function context graph: A simple plot of "location" that could include a hashes of portions of call stack and depth
        </div>
    </div>

    <!-- right-side panel -->
    <div style="float:left; width:30%; min-height:200px">
        <!-- panels and profiling -->
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Top Cells</span>
             <table id="topCellsTable" style="width:100%; margin-top:10px; font-family:monospace; text-align:right; visibility:hidden cellpadding=0 cellspacing=0">
                <thead>
                    <tr style='background-color:#c0c0f0;'>
                        <th>Track</th>
                        <th>Profile</th>
                        <th>Table (incl. bank)</th>
                        <th>Row</th>
                        <th>Bank</th>
                        <th>Value</th>
                    </tr>
                </thead>
                <tbody id="top-cells-tbody">
                </tbody>
            </table>
        </div>
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Tracked Cell(s)</span><em></em>
            <table id="trackedCellsTable" style="width:100%; margin-top:10px; font-family:monospace; text-align:right; visibility:hidden">
                <thead id="tracked-cells-thead">
                </thead>
                <tbody id="tracked-cells-tbody">
                </tbody>
            </table>

            <!--
            <div class="prototype-warning-small">This box is for tracking cells throughout time and seeing some concrete statistics.<br/>
            For example: it may show
            <li> A table of all stats for that cell over the selected time range</li>
            <li> A list of branches including opcodes which touch them</li>
            Cells tracked in this box will have one the current selected 'stat' displayed on the timeline chart. It may also be highlighted in the heatmap? Maybe the heatmap color should be displayed beside each row<br/>
            There may be a limit of 'enabled' tracked cells.
            </div>
            -->
        </div>
        <!-- Code information -->
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Branch Imem</span><em style="font-size:12px;">(only branches <b>trained</b> and within selection range)</em> <input type="button" onClick="renderBranchProfileList();" value="generate (clears filters)" />
            <span id="branch-imem-stale" class="stale-notice"></span>
            <!--input type="button" onclick="clearBranchFilters();" value="clear branch filters" />-->
            <table id="branchImemTable" style="width:100%; margin-top:10px; font-family:monospace; text-align:right;visibility:hidden" cellpadding=0 cellspacing=0>
                <thead id="branch-imem-thead">
                </thead>
                <tbody id="branch-imem-tbody">
                </tbody>
            </table>
            <!--
            <div class="prototype-warning-small">This box will contain a of the BRANCHES seen within this window ranked by some metric and some of their information and statistics. Selecting a branch here should cause it to appear in the call stack box below and potentially could filter the 'top cells' list to include cells effecting that branch</div>
            -->
        </div>
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Selection Stat Summary</span><em style="font-size:12px;">(only selected range)</em> <input type="button" onClick="renderMispredictSummary();" value="generate" />
            <span id="selection-stats-stale" class="stale-notice"></span>
            <table id="selectionStatTable" style="width:100%; margin-top:10px; font-family:monospace; text-align:right;visibility:hidden" cellpadding=0 cellspacing=0>
                <thead id="selection-stats-thead">
                </thead>
                <tbody id="selection-stats-tbody">
                </tbody>
            </table>
        </div>
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Heatmap Row Profile:</span>&nbsp;&nbsp;
            <span id="hm-row-profile-coords" style="font-size:18px; font-family:Arial; letter-spacing:1px;"></span>
            <br/>
            <em style="font-size:12px;">(only events within selection range. Create from <u>Branch Trace</u> row stats or <u>Tracked Cells</u> or <u>Top Cells</u>)</em> 
            <span id="hm-row-profile-stale" class="stale-notice"></span>
            <table id="hm-row-profile-table" style="width:100%; margin-top:10px; font-family:monospace; text-align:right;visibility:hidden" cellpadding=0 cellspacing=0>
                <thead id="hm-row-profile-thead">
                </thead>
                <tbody id="hm-row-profile-tbody">
                </tbody>
            </table>
            <!--
            <div class="prototype-warning-small">This box will contain a of the BRANCHES seen within this window ranked by some metric and some of their information and statistics. Selecting a branch here should cause it to appear in the call stack box below and potentially could filter the 'top cells' list to include cells effecting that branch</div>
            -->
        </div>
        <!-- TODO: Add a call-stack display -->
        <!--<div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Stack</span><em>(of selected branches)</em>
            <div id="callStackDiv"></div>
        </div>-->
        <div style="border:1px solid #808080; padding:8px;">
            <span style="font-size:18px; font-family:Arial; letter-spacing:1px;">Heatmap Distributions</span>
            <div class="prototype-warning-small">This should contain some histograms (per table) showing distribution of heatmap values. Similarly, a few percentile lines could be drawn on the line-plot to the very left to capture similar information</div>
        </div>
    </div>    
  </div>
  <hr>
  <strong>DEBUG:</strong>
  <div id="debugDiv" style="height:500px; overflow:scroll;">Loading...</div>
  <script>

    
    
  </script>
</body>